From 201e0a1520f51231b79563be9234a398465a7669 Mon Sep 17 00:00:00 2001 From: Curry Date: Sat, 18 Jul 2026 19:22:30 +0800 Subject: [PATCH] fix(workflow): unblock draft compile previews from plan-IR strictness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integration suite had 5 red tests in the patch/import/demand-draft family, all one root cause cluster: compile previews (always PlanGraph(draft=True)) were failing canonical-lifecycle checks meant for finalized graphs. - plan_ir/validation: skip orphan_merge on draft graphs — imports and demand drafts legitimately hold partially wired merges the operator finishes in the studio; non-draft validation (POST /plan-ir/validate, publish gates) keeps the strict rule - plan_ir/validation: port-type check now honors 'unknown' as a wildcard, same semantics as the compiler's own _types_compatible — imported external.tool.capability nodes (fallback type 'unknown') stay connectable - compiler: intelligence.output.inbox input port items[] -> recordCandidate[], aligning with its kind-level fallback and the collection-result sibling; the stale type made normalize -> inbox unconnectable tests/integration: 383 passed, 5 skipped (was 378/5 red) --- backend/plan_ir/validation.py | 19 ++++++++++++++++--- backend/workflow/compiler.py | 6 +++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/backend/plan_ir/validation.py b/backend/plan_ir/validation.py index 12de8e2..34aac28 100644 --- a/backend/plan_ir/validation.py +++ b/backend/plan_ir/validation.py @@ -204,7 +204,15 @@ def _check_orphan_merges( ) -> list[PlanValidationError]: """A merge node exists to combine two or more upstream branches — one with fewer than two connected inputs is not merging anything and is - reported as orphaned (PRD story 23 / issue 01 acceptance criterion).""" + reported as orphaned (PRD story 23 / issue 01 acceptance criterion). + + Draft graphs are exempt: compile previews, external-runtime imports and + demand drafts (all compiled with ``PlanGraph(draft=True)``) legitimately + hold partially wired merges the operator finishes in the studio. The + strict rule keeps gating non-draft validation (``POST /plan-ir/validate`` + with ``draft=False`` and future publish gates).""" + if plan.draft: + return [] errors: list[PlanValidationError] = [] incoming_count: dict[str, int] = {n.id: 0 for n in plan.nodes} for e in plan.edges: @@ -233,7 +241,10 @@ def _check_port_type_mismatches( plan: PlanGraph, nodes_by_id: dict[str, PlanNode] ) -> list[PlanValidationError]: """An edge's source-port type and target-port type must match, unless - either side is declared "any". Dangling edges (already reported by + either side is declared "any" or "unknown" — the same wildcard semantics + the workflow compiler's own edge check uses (``_types_compatible``), so + imported ``external.tool.capability`` nodes (fallback port type + "unknown") stay connectable. Dangling edges (already reported by ``_check_dangling_edges``) are skipped here to avoid duplicate noise.""" errors: list[PlanValidationError] = [] for e in plan.edges: @@ -245,7 +256,9 @@ def _check_port_type_mismatches( tgt_port = next((p for p in tgt.inputs if p.name == e.target_port), None) if src_port is None or tgt_port is None: continue # already reported as unknown_source_port / unknown_target_port - if src_port.type != tgt_port.type and "any" not in (src_port.type, tgt_port.type): + if src_port.type != tgt_port.type and not ( + {"any", "unknown"} & {src_port.type, tgt_port.type} + ): errors.append( PlanValidationError( code="port_type_mismatch", diff --git a/backend/workflow/compiler.py b/backend/workflow/compiler.py index 27f9b5a..bee753d 100644 --- a/backend/workflow/compiler.py +++ b/backend/workflow/compiler.py @@ -84,7 +84,11 @@ class _PortContract: [_PortContract("out", "output", "storedItems[]", required=False)], ), "intelligence.output.inbox": ( - [_PortContract("in", "input", "items[]")], + # recordCandidate[] like its kind-level fallback (kind=inbox/sink + + # store) and the sibling collection-result contract — the old + # items[] here predated the recordCandidate[] type chain and made + # normalize -> inbox unconnectable. + [_PortContract("in", "input", "recordCandidate[]")], [_PortContract("out", "output", "storedItems[]", required=False)], ), "intelligence.output.webhook": (