You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A GraphSpec's declared input_type/output_type is never checked — and check_subgraphs trusts it #21
A fifth place, found while probing #12, and it is worse than the four in #1 because a check that does run reads it as its oracle.
The failure, named first
A GraphSpec declares input_type / output_type. Nothing compares them to the variables the
edges at START and END actually carry. So the design's public boundary — the one field a parent
trusts — can say anything.
classLying(Greeting): # every edge in Greeting carries strinput_type, output_type=int, intLying().coherence_check() # []Lying().coherence_check(trim_only) # []Lying().render(trim_only).run_sync(inputs=" a b ") # 'Hello, a b!' <- a str
Clean, renders, runs, returns the wrong declared type. On its own that is a dead field, which
would be a small thing.
It is not a dead field — check_subgraphs believes it
_port_type compares a parent node's contract against child.input_type / child.output_type.
Those are exactly the values nothing verifies, so the check that exists to prove "the child fits
the node" proves "the child's claim about itself matches the node":
child alone: [] # Child declares output_type=int; its edges carry str end to end
parent+child: [] # the parent node declares an int output. check_subgraphs passes.
declared int, actually: str 'HELLO'
A subgraph binding that genuinely does not fit its node goes green, and the type error surfaces
wherever the parent's next step touches the value — far from the declaration that caused it.
That is the same distance-from-cause failure .claude/rules/deploy.md records for a stubbed
Dockerfile import.
Why it is this repo's recurring defect, exactly
The rule already exists and is already written down — _port_type refuses a multi-output
boundary with a careful message. What is missing is that nobody checks the thing being compared
against. A check narrower than its claim, the seventh time: the comparison is real, the oracle
is unvalidated, and the result reads as a verdict about the child.
Proposed fix
A check_boundary_types: for each edge leaving START, the carried variable's type must match input_type; for each edge reaching END, output_type. Blocking, not NOT CHECKED — both
values are declared, so there is nothing unavailable to excuse a stated gap.
Two cases to get right, and both already have precedent in _port_type:
Several edges reach END (a step with 2 outputs, or two branches of a decision). They must
all match, and if they carry different types the design has no single output_type — which is
the refusal _port_type already words for subgraph boundaries.
A step battle wraps a StepSpec in a one-node graph and synthesizes exactly these two
fields. Nothing would catch getting that synthesis wrong. Land this first, or alongside.
check_boundary_types is the 13th rule, and the eighth that needs nothing implemented. The two
measurements in the body both go red now — the lying subclass of a real example, and the subgraph
that passed on its own claim.
Two things found on the way, both real and both in the PR:
_produces called list[int] vs list[int] undecidable.list[int] is list[int] is False, and a parameterised alias was never compared against a bare declared type. Fixing both
is what keeps this check clean on all 14 designs in examples/ rather than printing a permanent NOT CHECKED on parallel.py — i.e. the alternative was editing an example to suit a new check.
_type_name rendered list[int] and list identically. Its docstring said generic aliases
have no __name__; since 3.10 they do, and it is the bare origin. So a finding comparing those
two types read as a complaint that list is not list — found by writing this check's message,
which compares exactly that pair.
Two decisions left open rather than taken:
about="" on the findings. A port name would be a fifth kind of value in a field test_every_finding_names_something_the_caller_can_look_up resolves — a vocabulary change.
A fifth place, found while probing #12, and it is worse than the four in #1 because a check that
does run reads it as its oracle.
The failure, named first
Clean, renders, runs, returns the wrong declared type. On its own that is a dead field, which
would be a small thing.
It is not a dead field —
check_subgraphsbelieves it_port_typecompares a parent node's contract againstchild.input_type/child.output_type.Those are exactly the values nothing verifies, so the check that exists to prove "the child fits
the node" proves "the child's claim about itself matches the node":
A subgraph binding that genuinely does not fit its node goes green, and the type error surfaces
wherever the parent's next step touches the value — far from the declaration that caused it.
That is the same distance-from-cause failure
.claude/rules/deploy.mdrecords for a stubbedDockerfile import.
Why it is this repo's recurring defect, exactly
The rule already exists and is already written down —
_port_typerefuses a multi-outputboundary with a careful message. What is missing is that nobody checks the thing being compared
against. A check narrower than its claim, the seventh time: the comparison is real, the oracle
is unvalidated, and the result reads as a verdict about the child.
Proposed fix
A
check_boundary_types: for each edge leaving START, the carried variable's type must matchinput_type; for each edge reaching END,output_type. Blocking, notNOT CHECKED— bothvalues are declared, so there is nothing unavailable to excuse a stated gap.
Two cases to get right, and both already have precedent in
_port_type:objecton either side is Four places the declaration outruns the check #1's item 3 — it opts out silently today. Same treatment:NOT CHECKED — …, visible.all match, and if they carry different types the design has no single
output_type— which isthe refusal
_port_typealready words for subgraph boundaries.A step battle wraps a
StepSpecin a one-node graph and synthesizes exactly these twofields. Nothing would catch getting that synthesis wrong. Land this first, or alongside.
Repro
Measured 2026-10-06 against
0643547, pydantic-graph 2.35.1. Relates to #1, #12, #9.