Repository navigation
[GEN-E3] union binding returns its decisions; nested-union and object-or-array defects refuse or bind correctly (PR 1/3) - #425
Merged
Conversation
…ent union defects refuse by name Marked, error and object binding move out of SchemaPlanBinder (810 -> 166 lines) into MarkedUnionBinder, ErrorUnionBinder and ObjectModelBinder. Each returns its plan and its membership edges, and UnionInheritanceGraph assembles base types and fixed markers from them, replacing the shared inheritance and fixed-marker accumulators and their post-pass. Every order the emitted code depends on is kept, and a test pins each: record base lists (graph-key order, then branch order, the error union last), error variants by type name, and first-claim hoisted carriers by depth before name. Generated output is byte-identical. - P36: a nested union under two parents, or a stream-cause union with a union parent, refuses with one named error instead of silently taking the last parent. - P37: a free-form object-or-array union binds to JsonElement, the representation upstream's schema admits; a typed object-or-array pair stays a structural union. - P45: each error-spine spelling has one owner. The '_tag' wire literal is owned on the Ingestion side and ErrorMarkerPolicy refers to it, keeping the Binding -> Ingestion direction.
This was referenced Oct 17, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Union binding becomes named collaborators that return their decisions, and two latent union defects refuse by name instead of producing wrong code. Generated output is byte-identical.
Closes: P39, P36, P37, P45. This is GEN-E3 PR 1 of 3, following the approved plan (
faz4/GEN-E3/plan.md, amendments included). PR 2 (operations, with #414) and PR 3 (graph and statics) follow serially.What changes
One binder per union kind.
SchemaPlanBindergoes from 810 lines to 166. Each new binder returns its plan and its membership edges:MarkedUnionBinder(321 lines);ErrorUnionBinder(94);ObjectModelBinder(129).The graph replaces the accumulators.
UnionInheritanceGraph(70 lines) assembles base types and fixed markers from the returned edges. It replaces the sharedinheritanceandfixedMarkersaccumulators and their post-pass, which are gone (git grep).Errors stay on the existing collector. Per amendment F3, no result-local error batches yet; those are PR 2's
FacetResult. The new records areUnionBindingResult,ErrorUnionBindingResult,SchemaBindingContextand oneUnionChildEdge, which carries a nullable fixed marker.The error union is not a schema node. The graph never resolves parents, stream-cause membership or a fixed marker for the synthetic error union. A real component keyed
OpenCodeErrortherefore cannot wire itself ontoIOpenCodeError. Without this, that component would make the error union inherit from itself, and the base-chain walk loops. A regression row pins it. Master never wired the error union, so this closes a gap the new graph would otherwise open.P36: two shapes refuse with one named error ("nested union has multiple parents 'A' and 'B'") instead of silently keeping the last parent:
The refused union's membership claims are dropped before the graph is published, so no "absent union" errors follow from its leaves.
P37: an unmarked union of a free-form object and an array of unrestricted values binds to
JsonElement. Before, it bound to an unspecified object, which cannot hold the array. A typed object-or-array pair, such asConfig.ModelEncoded.cost, stays a structural union. Nothing at the pin has the free-form pair. The maintainer was told about this direction and may reverse it (spec decision 1).P45: each error-spine spelling has one owner.
ErrorMarkerPolicyowns the C# spellings:Tag,IOpenCodeErrorand the unknown names._tagwire literal is owned on the Ingestion side, byErrorStyleClassifier.WireName, andErrorMarkerPolicy.WireNamerefers to it. Nothing in Ingestion references Binding, so the Binding → Ingestion direction holds."IOpenCodeError"literals now read the owner.Canon:
protocol-and-generation.mdgains the P37 sentence, as its own paragraph after the standalone-not rules.Orders (byte-identical output depends on them)
Each order the emitted code depends on is kept, and a test pins it:
ModelEmitterTests: emitted base listcmp) → 9/9SpecBinderTestsTag→ red; restored → greenUnionMemberHoistBinderTestsThe union variant order, registry order and diagnostic text and order are unchanged; both reviews checked them side by side against master.
Tests
P37 (1 → 1).
Both P36 rows went red twice:
They now assert the full ordered
Errorslist, exactly one error.The
OpenCodeError-key regression: it was red with a false multiple-parent refusal plus two absent-union errors, and is green as a clean bind.VariantsandRefusalsfor an admitted schema, a schema with no name and a schema with a bad marker. Deleting either recording call turns it red. PR 2'sErrorMapBinderconsumes them.JsonElement, and the test name…As_An_Unspecified_Object→…As_JsonElement. No other existing assertion or snapshot changed.CollectionOrdering.Matching;Contains, and noIsTrueoverAny/All;Clone baseline (one fingerprint swap, proven)
tests.baseline.jsonexchangesccafdf2207f06e4ffor0555dcc5c34a8475. The pair isSpecBinderTests.cs:1673-1678↔:1779-1784, two pre-existing arrange prefixes. P37's allowed test rename shifted the token window.SpecBinderTests.csrestored at this head,verify-clonespasses against master's baseline with 0 new clones. The clone count is the same, 664.Budget (against the merge-base)
Local check and review
verify-clonespassing (with the swap above);--help;generate --verifycurrent; the whole Tools.Tests project (guards included) at 2004/0. The style pass caught two using-order slips (ErrorUnionBinder,ModelEmitterTests). After the fix, both format passes, the all-framework build,ModelEmitterTests(9/0) andSpecBinderTests(157/0) were rerun green. Bun 1.4.3, off the 1.4.2 pin. CI is the full gate.generate --verifyon the committed tree for both runs (manual §5.3). The only red line wasverify-cloneson the proven swap above.generate --verifyhit a shell timeout mid-format. The rerun recovered to a byte-identicalsrc/through GEN-E8's re-runnable write, with no hand edits.Provenance: Generator lane / opencode dispatch harness (openai/gpt-6.1-sol-1m) for the plan, the implementation and the fix-up, coordinated, reviewed and finished in Claude Code.
Need help on this PR? Tag
@codesmith-botwith what you need.