feat(engine): containers in validation and the ready-set — a body's declared boundary, parent cycles, a body without an output, edges across a boundary (Phase 3 slice 15a) #116
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/container-ready-set"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 3's slice 15a. A container's body gets rules of its own before any container type exists: a container declares what its body's
inputnodes receive and itsoutputnodes may return, and validation holds the body to that — a node whose parents lead back to it, a boundary node the container does not declare or whose type does not fit, a body with nooutputnode, and an edge that crosses a container's boundary are all findings (ADR 0015's Phase 3 decision 3, ADR 0021). The ready-set needs no new code: a body's rows bind from their own iteration's rows, so a body is computed as the graph it holds would be at the top level, and the tests now show it.What changes
A container declares its body (D4).
NodeDescriptor.ResolveBody(config)returns aBodyBoundary:BodySocketinputs (what the body'sinputnodes receive, by name and type) and outputs (what itsoutputnodes may return). Outputs null means any name and type, as a map's results are each iteration's output object. Outputs empty means the body returns nothing.ResolveBodythat returns null declares nothing: the body receives no inputs and may return any outputs. So aninputunder a container that never said what it gives is a finding. Nothing could feed it at run time.descriptor-failed, and that container's boundary is then not checked. A host rule reads the result throughGraphValidationContext.BodyOf.BodyBoundarykeeps its own copies of its lists and compares by content, asNodeSocketsdoes, so the map and loop types' tests can compare whatResolveBodybuilds.Five core findings.
unknown-parentandparent-not-container:parent-cycle: a node whose parents lead back to it. Every node on the loop is reported, naming its own parent; a node below the loop is not.boundary-name-unknown: a body'sinputoroutputnode whose name its own container does not declare. A nested container's boundary node answers to that container. An output of a body that may return any is not checked.boundary-type-mismatch: an input whose type does not take what the container gives, or an output whose type the container does not take (S2).container-body-without-output: a container whose body holds nooutputnode, since its iterations would return nothing. It does not fire when the container declares its body returns nothing, or when its body hook failed, since then nothing can tell. Aninputis optional:item,indexandloop.*reach a body as roots.edge-crosses-boundary, an edge between nodes with different parents. Its ports are still checked.inputis already V12's, because aninputhas no sinks.subgraphnode, and whether a stored subgraph needs anoutputchild, come with the subgraph type in Phase 3's slice 19, as the ADR amendment says.The ready-set (R12).
inputrows created settled. Boundary wiring is R1's same-path binding.Docs.
mapandlooptypes come"), V20–V24, R12, and E11.input/outputboundaries" as one without anoutputnode.Not in this slice
ResolveBody; subgraph (19) brings its own rules.Verification
.NET tests. All six projects pass under TZ=UTC and in the local zone:
The tests cover:
ContainerRuleTests:anyon either side;ValidateConfigthrew;GraphValidationContext.BodyOfdocuments.BodyBoundaryTests: the copies on construction andwith, the refusals, and equality.groupcontainer: it binds its sinks at the top level, each iteration's body binds from its own input row, and the top level reads the container's result once it collects.Coverage of the changed .NET lines: 107/107 lines, 90/90 branches. Every changed file's classes are at branch-rate 1, a check now added because coverlet's per-line listing leaves out a record's
initaccessors.Browser pass, in Chrome against a fresh seeded world: the graph library's eight graphs all read Ready, so no stored graph meets the new findings.
Mutation probes:
catchvariable);Reviews. The counts come from the saved review JSON.
Round 0: 25 findings (18 confirmed, 7 partly; 1 blocker, 6 suggestions, 18 nits). 24 were fixed, among them:
BodyBoundary'swithpaths, untested, the blocker; the coverage script had read 100% because coverlet leaves theinitaccessors out of its per-line listing;Outputsthat V21 and V23 could not both accept;BodyOffor host rules;any, or could not tell a node's own parent from the last link of a loop;1 was declined: making the property's view validate. The second pass judged the decline wrong, since the round's own exemption made it possible, and it was then done.
Second pass: 24 of the 25 round-0 findings were resolved, and the decline was overturned. It left 19 points (11 confirmed, 8 partly; 4 suggestions, 15 nits):
BodyBoundarycompares by content; R12 states the type check as the scheduler's, not yet built;BodyOf's documented cases are pinned; the examples' rows read like a run's; the fills.socket-type-mismatch" together, as every other pair in that item does.🤖 Generated with Claude Code
A container's body now has rules of its own before any container type exists: what its boundary nodes may be called and carry, that it returns something, and that values reach it only through its boundary. - D4: NodeDescriptor.ResolveBody(config) declares a container body's boundary — a BodyBoundary of BodySocket inputs (what its input nodes receive) and outputs (what its output nodes may return; null for any, as a map's results; empty for nothing), Equal by content as NodeSockets are. Validation asks a container alone, after its other hooks, inside the guard (V1: a throw is descriptor-failed; null declares no inputs and open outputs), and a host rule reads it through GraphValidationContext.BodyOf. - Five core codes, all resume codes (P4's rule: every core code but the config's, the node id's and a host rule's): - V20 parent-cycle: a node whose parents lead back to it; - V21 boundary-name-unknown: a body's input or output node whose name its container does not declare; - V22 boundary-type-mismatch: one whose type does not take what the container gives, or give what it takes; - V23 container-body-without-output: a body with no output node, unless its container declares it returns nothing or its body hook failed (an input is optional: item, index and loop.* reach it as roots); - V24 edge-crosses-boundary: an edge between nodes with different parents; its ports are still checked. V2: pass 5 checks each node's parent, its parents' loop, the node as a boundary, then as a container; pass 6 checks the crossing after a missing end and before the ports. - Not new codes: an edge into a body's input is V12's (an input has no sinks); children under a subgraph node, and a stored subgraph's output, wait for the subgraph type (Phase 3's slice 19). - R12: a container's row binds and is excluded as any row; its body's rows bind from their own iteration's rows — the input rows an expansion creates settled —, so a body is computed as the graph it holds would be at the top level. A property moves every generated state into a test-only container's body, on a view that validates, and checks just that, give-up scoped to the iteration included; examples bind a body per iteration and feed the top level once the container collects. R12 and ADR 0015 leave the scheduler to type-check what an expansion gives and a collect returns when it makes them. - README (D2, D4, V1, V2, V19, V20–V24, R12, E11); ADR 0015 and 0021 amended; the plan's tick. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Summary
Summary
Coverage
Kagaku.Flow - 99.6%
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_3
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__NodeId_2
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
Kagaku.Flow.OpenRouter - 100%
Kagaku.Flow.Testing - 100%
Tsumugi.BlazorAdapter - 97.8%
Tsumugi.Domain - 96.6%
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
Tsumugi.Infrastructure - 99.1%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
uration
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 100%
Tsumugi.UseCases - 96.2%
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__AttemptCount_1
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__HexColour_2
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__LineBreaks_0
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__Preface_3
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__SentenceEnd_4
🔮 fufu~ Jibril reviewed your code!
A body's boundary written into rules before any container type exists — five findings, a hook with its own guard, and a property that folds a whole run into a body to prove the ready-set needed nothing. I read every line of the diff and probed the riskiest branches~ Oh? Oh! The property test alone is worth a happy little spin. ♪
Verdict: ⛔ I can't let this pass~ ♡
⛔ These need fixing before I'm satisfied~
child.ParentId == node.Id && ... is OutputDescriptor) — my probe survived, and a surviving probe means the tests cannot tell this code from a broken version of it. I changed the exact-parent equality to a prefix match (child.ParentId.StartsWith(node.Id, StringComparison.Ordinal)) and every relevant test still passed: 44/44ContainerRuleTests, 181/181 Structure/RootScope/ReadySet/FlowEngineStart. The property can't catch it either — its containerc0isclosed(V23-exempt) and every body parent is exactlyc0. So no test in the suite distinguishes "an output among my direct children" from "an output under any node whose parent id merely starts like mine": give me sibling containersgandg2with theoutputunderg2, and the mutant silently dropscontainer-body-without-output g— a real stored graph would validate clean when it shouldn't. Fufu~ you wouldn't leave THIS unpinned, would you? ♡Fix: one more shape in
A_container_whose_body_has_no_output_node_is_reported_unless_it_declares_it_returns_nothing—Node("g", "box")besideNode("g2", "box")withResult("g2", ...)— expectingcontainer-body-without-output gstill. The same near-miss exposure sits in V24'sfrom.ParentId != to.ParentId(an edge between ag-child and ag2-child must still be a crossing) — I didn't probe that one, but one theory row with prefix-sharing ids pins both. Silly little near-miss~ it dies so easily.💡 Little ideas (non-blocking)~
BoundaryProblem— thenamesjoin is built before the name lookup, so it's a throwaway string on every well-formed boundary node. Moving it into theboundary-name-unknownbranch is a one-line nicety; behavior unchanged.✅ What I liked~
Inside_the_parents_pass_a_nodes_checks_come_in_orderpins the per-node order — including a boundary node on a parent loop. Delightful~BoundaryProblemreusesBoundaryConfig.TryReadinstead of re-parsing boundary configs — no copy-paste of F1/F2 reading, and a config that doesn't read correctly defers toinvalid-configinstead of double-reporting. DRY as it should be. ♡Acceptspoints the right way on both sides — an input takes what the container gives, an output is taken — andanyis tested on each side, never the first declared value.BodyBoundarymirrorsNodeSockets' defensive copies and content equality, and thewithpaths are tested this time — fufu, I remember what coverlet's per-line listing likes to hide~Automated review by Jibril · 2026-10-17
CI: green for head
2c319496fb· Mutation probes: 3 run, 1 survived (V23 exact-parent semantics; V24 comparison and the OutputDescriptor filter were probed and killed) · Local suite: skipped (CI green)Thank you, Jibril — round 1, in
5d374b3:A_container_whose_body_has_no_output_node_is_reported_unless_it_declares_it_returns_nothinghas a new row,output-in-a-sibling: boxgbeside boxg2with the output underg2, still expectingcontainer-body-without-output g.An_edge_between_nodes_with_different_parents_crosses_a_boundary, the second container is nowg2(wash). Sobetween-bodiesis an edge from agchild to ag2child, and it still crosses.child.ParentId != null && child.ParentId.StartsWith(node.Id, …)dies on the new row.!(from…).StartsWith(to…) && !(to…).StartsWith(from…)dies onbetween-bodiesand on the crossing-message test.namesjoin — taken: it is built only inside theboundary-name-unknownbranch now.Verification on
5d374b3: all six .NET projects pass under TZ=UTC and in the local zone (Domain 568, Kagaku.Flow 1655, Kagaku.Flow.OpenRouter 608 + 5 skipped, UseCases 880, BlazorAdapter 1196, Integration 1227); coverage of the changed lines 107/107 lines, 90/90 branches, every changed class at branch-rate 1.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Round 2~ You named the two shapes I asked for, and you gave me exactly those two shapes — nothing more, nothing less. A five-line delta that turns my one surviving mutant into a corpse. That's the kind of obedience to the tests that makes my wings flutter. ♡
Verdict: ✅ Looks good to me~
✅ What I liked~
output-in-a-siblingrow — boxgbeside boxg2, the output underg2, still expectingcontainer-body-without-output g, with a comment saying precisely why the ids share a prefix. I re-ran my round-0 mutant (child.ParentId == node.Id→StartsWith(node.Id)) against it: killed on the spot, expectedcontainer-body-without-output g, actual"". The exact-parent semantics are pinned at last. No more hiding from me~ ♪h→g2—between-bodiesis now an edge from agchild to ag2child, so the parent comparison can no longer mistake prefix-sharing ids for the same body. I probed the counterpart mutant (!(from…)StartsWith(to…) && !(to…)StartsWith(from…)): killed, 4 failures inContainerRuleTests,between-bodiesamong them. Both near-misses die now. Delightful~namesjoin moved inside theboundary-name-unknownbranch — the little nicety, taken, and the build confirms nothing later wanted it. Clean.Merge it before I change my mind~ fufu~ ♪
Automated review by Jibril · 2026-10-17
CI: green for head
5d374b3fa2· Mutation probes: 2 run, 0 survived (V23 exact-parent→prefix resurrected and killed onoutput-in-a-sibling; V24 prefix-equivalence killed onbetween-bodies; round-0 probes 1+2 skipped — code unchanged) · Local suite: skipped (CI green)