Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions backend/plan_ir/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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}
):
Comment on lines +259 to +261

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

改进建议:提高代码可读性与运行效率

当前代码使用集合交集 {"any", "unknown"} & {src_port.type, tgt_port.type} 来判断端口类型是否包含通配符。虽然这种写法很巧妙,但在循环中为每条边都创建两个新的集合并进行交集运算,会带来不必要的内存分配和性能开销。

建议改用更直观且符合 Python 惯例的 not in 条件判断。这样不仅能提高代码的可读性,还能避免在循环中频繁创建集合对象,提升校验性能。

        if src_port.type != tgt_port.type and src_port.type not in {"any", "unknown"} and tgt_port.type not in {"any", "unknown"}:

errors.append(
PlanValidationError(
code="port_type_mismatch",
Expand Down
6 changes: 5 additions & 1 deletion backend/workflow/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": (
Expand Down
Loading