Behavior-preserving code-quality cleanup across the Dify Builder engine:
extract emit_canvas, build_change_set, mint_checkpoint, action_kind, and
merge_known_keys into handlers_fix.py (shared by handlers_build.py and
handlers_edit.py); extract _add in repository.py for the id-preservation
idiom; extract _make_edge (and unify the node/edge id-collision loops
into _next_unique_id) in graph_ops.py; extract _sync_graph_only in
dify_port.py. No behavior, generated ids, or wire output changed — same
387-test suite passes, ruff clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FINDING 1: Updated module docstring in handlers_build.py to accurately
reflect that build.complete's completion summary is emitted by the
governance-tail handlers (handle_governance_feedback for automatic/disabled
policies, or handle_await_learning for the ask policy).
FINDING 2: Strengthened two per-policy governance tests to include notice
kind assertions:
- test_governance_automatic_learns_and_reaches_complete: add assert "notice" in kinds
- test_await_learning_accept_learns_and_completes: add assert "notice" in kinds
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework handle_governance_feedback into the 3-policy branch (automatic/
disabled/ask) reading fc.skill_learning_policy, and add handle_await_learning
to resolve the ask-policy accept_learning/skip_learning turn. Both funnel
into a shared _emit_completion helper for the build-complete summary.
Registered BUILD_AWAIT_LEARNING in build_registry().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The runner's check_recovery/recovery_continue/recovery_restart short-circuit
committed the transition and returned immediately, without entering the
advance loop. recovery_restart in FIX/FIX_CHECKLIST mode (the forced path for
structural_invalidating drift) lands on the *working* fix.diagnose /
checklist.diagnose entry state, which the runner must drive by running its
handler -- the early return stranded the session there with no UI actions.
Fix: rest only when the landed state is waiting/terminal (covers stop/resume,
check_recovery, recovery_continue, and BUILD/EDIT restarts unchanged);
otherwise consume the action and fall through to the existing advance loop.
Also: parity-test recovery.entry_state_for against the states
service.create_*_session actually assigns; add classify()-through-runner
coverage for the three remaining recovery classes and target_node_ids guard
branches; pin recovery_ref_for's message for config_only/structural_compatible;
and qualify dify_port.py's graph_ops delegations (module import) so
structural_fingerprint/node_ids no longer read as unqualified self-recursion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds RecoveryRef to the contract, recovery_ref_for() to recovery.py, and
wires it into get_session_view() beside the existing CheckpointRef pattern.
Regenerates contract_schema.json and the FE types.ts from contract.py
(additive only: RecoveryRef + SessionView.recovery). Also updates
test_wiring.py's exact asdict() round-trip assertion to include the new
recovery: None default, since dataclasses.asdict(SessionView) is exhaustive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every fc.last_snapshot_hash assignment across handlers_fix.py, handlers_build.py,
and handlers_edit.py now sets fc.last_structure_fingerprint alongside it, using
result.structure_fingerprint at apply-repair sites and
env.dify.structural_fingerprint(graph) at read-graph/restore sites.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
continue_adjusting/revert/retry_after_revert all return to
build.initial_plan without resetting the already-built graph. Re-walking
find_resources -> confirm_resources -> approve_plan then re-emits the same
fixed create_node/connect intents, and the second apply_repair raised
ValueError on the colliding node id, dead-ending the session at
build.plan_approval.
Fix: before applying, read the current draft graph and drop any
create_node whose node_id already exists or connect whose (from, to) edge
already exists, so a re-approve after a loop-back is a no-op apply_repair
rather than a crash. built_node_ids is still set from the full 4-node set
regardless of what was actually applied this pass.
Adds an end-to-end loop test via the real Runner covering both the
continue_adjusting and retry_after_revert paths, asserting the session
reaches build.execution with the graph still at 4 nodes / 3 edges.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>