Plan files are deleted on Done; this is the historical record. See Roadmap for in-flight work, or back to home.

Rewrite Changelog

  • R840 (325d3284e`+87b06ebc0` slice 1, 317b08b93 slice 2, 341a758a3 slice 3; In Progress → In Review 323454bb3, Done gate in this commit): The spec-by-example corpus became a folder of self-describing, fact-first SDL documents. All 57 fixtures live as graphitron/src/test/resources/corpus/<id>.graphqls with the prelude a document beside them; ClassifiedCorpus and the Java prelude string are retired, and CorpusDocuments loads the folder from the source tree under four floors (non-vacuity ratchet, listing agreement, every document claimed by the parameterized test, split invariant). Each document states what the fact store holds for it as @expectEquals(relation:, rows:) CSV blocks, read back out of the store and compared by anti-join in both directions, with name resolution through StoreCatalog, well-formedness, an assertable-population fence and CHECK-membership checks, planted regressions under each. code-generation-triggers.adoc collapsed from 2474 to 1208 lines: 32 generated-and-committed _example-<id>.adoc fragments are simultaneously the page’s includes and the emitted-names approval files, held by CorpusFragmentTest with three placement floors (a fragment with no document, a document with a projection and no fragment, a fragment no page includes), retiring ClassifiedDocTest, OutcomeBlockDocTest and the paste loop. Adding a worked example is now a document, a regenerated fragment and one include line, no .java edit. The per-axis @classified retirement waits on R682’s relations; each surviving tuple axis is a visible request for one.

  • R814 (b868b9947-e3ddcd419 the slice-3 promotion round, 9e567f89f the rework; In Progress → In Review 6852fa364, In Review → Ready 151cb1799, Done gate in this commit): docs/architecture/reference/code-generation-triggers.adoc described the classification walk it is organized around, a surface the tree calls "being drained, not a place to extend", and routed a contributor into it. The page is now organized as the chain the generator runs, capture to facts to verdicts to command rows to rendered units, with the verdict layer (intent_authored_field_claim and kin) as its closed vocabulary, a build-generated command-relation fragment that is never committed, a six-package ownership map replacing a Source Map whose opening claim was false, and thirty-two corpus examples each carrying a machine-rendered outcome block beside its SDL. Two guards landed with it, both against habitats nothing scanned: ArchitectureDocSymbolGuardTest resolves every backticked type span on an architecture page against the reactor classpath, and TransientCitationCheck grew a walked habitat over docs/architecture and docs/manual. Both carry anti-vacuous floors and neither carries a burn-down list, those having reached empty and been replaced by direct real-tree assertions. The promotion loop found more than redundancy: two reference rows claimed graphql-java’s default fetcher handled a coordinate the generator emits a method for, one row claimed a Relay trigger by field name where the generator matches by signature, and six type-side fixtures turned out to pin verdicts on schemas the build refuses, which is what a doc example running generation catches and a classification-only corpus does not. Goal 3 (every enumerable claim renders from a gated source, or is not on the page) is deliberately not met, and the reasoning was moved out of a commit message and into the item body at the Done gate’s insistence, which is what let it be carried here rather than deleted with the file. The remainder splits three ways with an owner each: the refusal rows are at their floor, since a success-only corpus has no example to subsume a build failure, and go to R842; the live generating rows go to R845, which states why waiting beats promoting, the paste loop being what R840 abolishes and the sealed leaf vocabulary the rows are written in being what R682 deletes, so an example authored now needs rewriting anyway; and rows a rendered block already subsumes are pure deletion rather than promotion, so the waiting argument never covered them and they were folded in. The Done gate bounced one round, on completeness rather than on code. Round 1 found two of the item’s four stated goals unmet with the stopping argument recorded only in a commit message, a thirteen-part narrative that answered how the work went rather than whether it was done, and four of the five archaeology fragments the item’s own survey had named still on the page as parentheticals inside cells that otherwise stated today’s behaviour. It also found a roadmap plan slug cited on a published page which the item’s own citation gate could not see, the two existing patterns anchoring on an id and on a roadmap/ path while a bare slug offers a regex nothing to match. The rework deleted all four fragments, folded in the @field(name:) scalar-child row after verifying both its spellings render, rewrote the state section into what shipped and what is left with owners, replaced the slug with the live anchor the code uses (Rejection.StubKey.VariantClass), and widened the gate rather than deferring it: a third pattern resolves a backticked span against the files actually under roadmap/, the backtick requirement being load-bearing because a live slug is already spelled out in the manual as an ordinary hyphenated adjective and a shape-only match would report English prose as a citation. Independent-session In Review → Done review, the reviewer sharing no session with any implementation commit: full reactor green under mvn install -Plocal-db. Completeness was checked against the item’s own four goals rather than the build, each verified against the tree: zero R<n> citations and zero resolvable bare-slug citations across both published trees, zero dangling symbols, zero surviving archaeology fragments, and the @field(name:) subsumption confirmed at the coordinate rather than taken on trust (Country.name under @field(name: "country") renders TABLE_COLUMN in the derived-layer example’s outcome block). The widened gate was checked for both directions and for the cost that would have made it unaffordable, the seven new cases covering a planted bare slug, a hyphenated prose collision that must not fire, a backticked span naming no item, and double-reporting when a path citation already fired on the line. The two blocking findings' repairs went beyond what the findings asked in one place worth recording: the gate hole could have been closed by editing one sentence, and was closed by making the guard able to see the shape. Round 1’s build observation was diagnosed rather than dismissed as a flake and filed as R843, the reviewer’s reproduction (passes alone, passes as a class, passes as a module, fails once in the parallel reactor) and the implementer’s diagnosis agreeing: DmlBulkMutationsExecutionTest seeds film_actor rows the splitTableField_bridgingConditionJoin assertion counts, against one shared database at parallelism 4. User-facing-doc check clean, no roadmap-internal marker surviving in either tree; retirement sweep skipped, the item declaring no retired vocabulary; no code-string assertions on generated method bodies, OutcomeBlockRendererTest.theBlockRendersNoCommandRowsAndNoBodies pinning names and parameter lists as the ceiling.

  • R832 (47cae33a8 the budget split, 72a09d416 the re-measured ceiling; filed 32d1299ac, Backlog → Spec 61a879a6d, Spec → Ready 24c63d94c, Ready → In Progress 47cae33a8, In Progress → In Review 053d577cb, In Review → Ready 6e4e0c3d4, Ready → In Progress 6d0e6b34d, In Progress → In Review 118fa378f; Done-gate review in this commit): CatalogRefreshTest’s single `WAIT_MS carried two independent quantities that happened to share a value, and splitting them by what each pays is what let either be argued at all. FIRE_CEILING_MS is a bound on failure, paid only on a red run because CountDownLatch.await returns the moment the latch counts down, so both positive awaits take it and a generous figure costs a green run nothing; QUIESCENCE_MS is a Thread.sleep the negative test pays in full on every green run, so it stays at sixteen times the debounce, a shorter window being what would turn a mis-wired late fire into a silent pass. Only the ceiling moved, from the inherited 1.6 s to 60.1 s, and the per-run cost is unchanged, which is the property that says the split landed on the right axis. The trigger this budget actually holds is real work, a cold SourceWalker parse through the Compiler Tree API plus the jOOQ writes that land it, where the SchemaWatcherTest the figure was inherited from triggers no-op countDown() runnables; that test keeps its own WAIT_MS, as the item’s out-of-scope section declares, with the disclosed gap that nothing enforces its triggers staying no-ops. The Done gate bounced one round, on the item’s own completeness instrument rather than on its code. The spec had made the headroom ratio the falsifiable evidence and said why, the measurement and the ceiling both living in the javadoc so that a reviewer could re-measure, divide and compare. The ratio did not survive that division: 854 ms and "roughly seventeen times" were a single quiet-machine sample presented as a measurement of a figure whose whole job is to survive a loaded one, and re-measurement under the module’s own four-way class concurrency put the refresh at 4,308 ms, about 3.5x, with two javadoc sentences false as written. The premise came from the approved Spec body rather than from the implementer’s diff, so the finding was a defect in what the gate had signed off, surfaced because this was the item’s first fresh-context reading. The rework took the free axis rather than the weaker claim: sixteen samples between 483 ms and 4,308 ms, and a ceiling raised to about fourteen times the worst of them. The history paragraph got sharper doing it, no longer citing a ratio against a sample but stating that the retired 1,600 ms budget falls under five of the sixteen, which is a checkable statement about the defect and the sharpest available evidence that the flake was real. Independent-session In Review → Done review, the reviewer sharing no session with any implementation commit: full reactor green under mvn install -Plocal-db (BUILD SUCCESS, 14 modules, none skipped, CatalogRefreshTest 3 of 3 in 4.1 s). Completeness was checked by performing the division the spec invites rather than reading it off the plan, all three figures holding: 60,100 over 4,308 is the stated fourteen, the retired budget sits under exactly the five named samples, and 1,600 is sixteen times the 100 ms debounce. The measurement was then retaken independently by the prescribed method, a System.nanoTime bracket around refreshJavaSources with the module’s classes four-way concurrent and three module suites looping alongside, at load average 35 to 41 on fourteen cores, heavier than the campaign the javadoc records: 491, 2,369 and 4,200 ms, inside the recorded spread, its worst not exceeding the recorded worst, and fourteen times under the ceiling. One mechanical premise the ceiling axis depends on and neither round had stated was checked too: nothing in the reactor sets a surefire fork timeout or a JUnit default timeout, so a red run really does reach the 60 s await and report the intended assertion rather than dying as a fork timeout, which is what makes "generous costs a green run nothing" true of the red run as well. Those loaded runs also corroborated the diagnosis, the class total stretching to 27.4 s while the refresh it brackets stayed under 5, which is why one sample never travelled. Every javadoc {@link} was verified against a live symbol, the reference gate reading main sources only and this being test source; the non-blocking observation that test-javadoc-no-fork is named in the root pom as the pair for test sources and not wired is left as Backlog material, as round 1 put it. User-facing-doc check skipped, the item touching no docs/ file; retirement sweep skipped, the item declaring no retired vocabulary; no code-string assertions on generated method bodies, the pins being store rows and a workspace queue.

  • R828 (8c03600 the four instruments and the page rule, 1472d69 the recipe’s outer loop, 3665bd1 the profiler’s self-sampling clause; driven end to end and rolled back across 5daaeaa-1ae9a48, Backlog → Spec 3e47227, spec review 6be3615, spec revision 6f5db57, Spec → Ready a815b86, In Progress → In Review f216109, Done-gate review 7ea2d25, rework returned 5d34775; Done gate in this commit): the store-performance skill no longer hand-rolls a timing the database was already keeping. SET QUERY_STATISTICS TRUE and INFORMATION_SCHEMA.QUERY_STATISTICS replace the System.nanoTime() probe outright, in step 2’s recipe and as the default in steps 3 and 5, ranking every relation a run touched instead of costing a probe method per relation, and reporting an execution count and a standard deviation with it. Three instruments join it, each placed against a question the skill already asked and could not answer: ConvertTraceFile at trace level 2 beside report-inline-multiplicity, as the measured counterpart to that tool’s static breadth ranking; trace level 3 behind the existing STORE_EXPLAIN guard, for the planner’s own per-index cost evaluation, which EXPLAIN ANALYZE never reports since it names the plan chosen and not the plans rejected; and org.h2.util.Profiler inside step 1, which keeps its place in the evidence order and stops being a dead end, its depth being a public field whose default of 48 truncates a view stack worse than the JFR default the step used to warn about. Four constraints ride with the statistics because each is a silent wrong answer: result reuse must be off or repeats are not repeats, MIN_EXECUTION_TIME reads 0.0 always and is neither a floor nor a tell, a row at EXECUTION_COUNT 1 ranks and does nothing else, and the statistics are the store’s rather than StoreConsole’s, that being a second database of linked tables. `docs/architecture/explanation/fact-model.adoc took the one addition that is an engine rule rather than instrument mechanics, that H2 reuses a repeated identical query’s result so a repeat is not a repeat until OPTIMIZE_REUSE_RESULTS is off, which is what decides whether any timing on a page made of timings is real; the skill cites it rather than copying the number down, and the citation policy gained a third category for figures that measure the tool rather than any relation. Both gates changed the shipped text. The Spec gate refused a tell that fired on every reading, MIN_EXECUTION_TIME having been named as the reuse discriminator while also being always 0.0, and got the cumulative-over-maximum ratio instead, whose error direction admits no false negatives; it also settled the blast radius, declining a second construction path on GraphitronModelStore because both settings are database-wide and two statements on the store’s own connection reach every reader a session mints. The Done gate caught the recipe looping over relations and never over executions, which delivered the single-run reading with no spread that step 1 opens by warning about, and made the mandatory reuse statement inert, since it does nothing measurable to a single execution.

  • R705 (ea73eed9 classification and emission, 7e107e36 the execution fixtures, the SQL baseline and the manual page, 9e5280d6 the lateral throw generalised off its original caller; filed and specced across 9f6924ab-eb976b34, Spec → Ready 218994c3, Ready → In Progress 46e0712d, plan departures recorded 5b74f76a, In Progress → In Review 21d07484; Done-gate review in this commit): a {condition:} hop is now legal in a @reference filter path, on both filter surfaces and at every position, mixed freely with {key:} and {table:}. The restriction read as a cardinality rule and was not one: the correlated EXISTS a reference filter emits returns each parent once however many rows the path reaches, which the new reverse-FK execution row pins for the first time, so foreign-key-ness was never buying uniqueness in filter position. Four mechanisms carry it. ServiceCatalog.terminalTableForReference becomes total (TableRef, not Optional), because a hop’s target resolves off its TableExpr and never off its on(), which turns the walker’s empty case back into the one thing it should mean, no such column, and hands two candidate-hint call sites real column names instead of an empty list. resolveConditionJoinTarget collapses its terminal-versus-intermediate fork into one rule keyed on the available source, declaredTarget or reflection on the method’s second parameter, with the positional test left at the call site so an intermediate hop still reflects; the preference direction is load-bearing, since preferring the declared target is what keeps a disagreeing concrete parameter a Check 2 finding rather than a resolution failure. FkHop retires in favour of a ReachPath(List<JoinStep.Hop>) carrier whose compact constructor is the filter rail’s single narrowing site, identity-keyed in the renderer’s alias map so per-occurrence locals survive. And the reach’s two dispatch points move onto the projection rail’s own arms, PathFragments.hopZeroCorrelation extracted from correlationWhere’s `OnParentJoin arm and emitBackwardBridging for interior hops, leaving ConditionGlueRenderer with no On switch of its own and JoinFragments below the narrowing where its charter puts it. Two things ride along. The output-side scalar @reference leaf, which failed with a generic unknown-column rejection for the same walker reason, is unlocked with pipeline and execution coverage rather than given a new guard to preserve an accidental rejection. And a correctness fix with its own acceptance row: reachExists never called appendHopFilters, so a {key:, condition:} filter path emitted an EXISTS without the author’s predicate and matched rows the schema excludes; it now emits them, pinned by a seeded execution case over rows sharing an FK slot but differing in the column the predicate reads. The surviving closure is @condition on an FK-target @nodeId field, restated from structural to Rejection.deferred because its old rationale was a mirror of the emitter precondition this item deletes; that carrier binds decoded id columns to the path’s FK slots and a predicate hop has nothing to bind them to. Docs: join-with-references.adoc retires the "Foreign-key hops only" rule and three stale claims (the condition: form is not a runtime-throwing stub, @reference(path: []) does not fail parsing, and a hop’s own predicate is emitted inside the filter’s EXISTS). Coverage across five tiers, the new ConditionGlueRendererTest among them. Independent-session In Review → Done review: full mvn clean install -Plocal-db green; no code-string assertions on generated method bodies at the pipeline tier; retirement sweep clean against all three layers the item declared; user-facing-doc check clean.

  • R673 (2a446ec9d the delivery, b3c437fe2 the rework round; spec body 1c30a4a78, spec review 5a5b59ac2, revision 86639003a, Spec → Ready e9d1349ea, In Review 4b8ea180f, In Review → Ready f13298dab, back In Review b4baed7fc; Done-gate review in this commit): a by-id lookup returning a multitable interface or union classified clean, generated without a diagnostic, and then accepted ids of one implementation only, each branch’s decode-or-throw helper carrying its own participant’s node type as a generation-time constant so the first branch threw on any id belonging to a different participant (reported as issue #526, and the reporter confirmed dispatch is what they expected). The fix dispatches on the decoded typeId. FieldBuilder.resolveNodeIdArgTargets is the one producer of the cross-participant verdict, sealed as SharedTarget (every participant decodes one node type, ThrowOnMismatch unchanged) or PerParticipant (a SequencedMap of per-participant decode refs, the map itself the dispatch fact, reached through one ParticipantFilterField.nodeIdArgDispatches() accessor; SequencedMap rather than the plan’s Map because the guard’s candidate list and helper registration are order-sensitive, a deviation the spec body states with its reason). A divergent branch lowers with the second NodeIdDecodeKeys arm, PruneOnMismatch, the mode ternary in the glue becoming an exhaustive switch on the seal, and ConditionGlueRenderer.appendPruningAnd renders the mismatch-vs-absent trichotomy per cell so a present-but-mismatched id prunes to DSL.falseCondition() while an absent argument leaves the branch unfiltered. The client error survives at the granularity where a mismatch really is a client mistake: MultiTablePolymorphicEmitter.nodeIdDispatchGuard, emitted ahead of stage 1 in both root fetchers including the @asConnection one, throws when no participant decodes an id, naming every candidate type in participant order, which also catches the right-prefix-wrong-arity id a matching branch’s prune helper would have turned into a silent empty result. A divergent nested-input bare-@nodeId leaf rejects at classification time out of the same producer, naming the leaf, the participants and their differing node types. Independent-session In Review → Done review across two rounds. Round 1 confirmed all seven decisions delivered and sent it back on one test-tier finding: the prune-mode list helper’s absent-or-empty fold was pinned only by a contains("nodeIds.isEmpty()") match on the emitted method body, the code-string assertion the development principles ban at every tier, so the one behaviour the fold exists to produce was unpinned where a regression would be caught. The rework deletes the unit case and pins the behaviour where it is observable, MultiTableFilterExecutionTest.dispatch_emptyIdList_leavesBothBranchesUnfiltered: occupantsByIds(ids: []) returns all seven occupants unfiltered, and dropping the fold turns that into zero rows; no code change was needed, as round 1 predicted. Round 2 verified the tier move and the tree: full reactor green under mvn install -Plocal-db against a private repo head and a per-worktree database, the new case run and passing, no code-string assertion on a generated method body left anywhere in the delivered tests, user-facing-doc check clean on the manual’s global-id and @nodeId reference chapters, retirement sweep skipped with no vocabulary retired.

  • R818 (b09afc1 the surface, the SDL type arm and both dispatch-matrix guards, bc84555 the directive-target arms; filed and Backlog → Spec cae3a1c, spec review round 1 1215dd1, revision d264f34, Spec → Ready ca3f174, Ready → In Progress folded into b09afc1, plan recorded cb70750, In Progress → In Review 6235f2e; Done-gate review in this commit): the language server answers textDocument/references, so an author about to rename a table binding or retire a @service class can ask what in the schema uses it instead of grepping for a word a description might also contain. referencesProvider is registered and GraphitronTextDocumentService.references answers on StoreRead.REFERENCES through the interactive door, the definition handler’s shape throughout: one span, one store read, one StoreAnswer switch, an empty list when the budget expires. Two arms behind it, keyed off disjoint syntax the way the definition chain already is. TypeReferences takes a cursor on a type declaration name or on a use of one and reads the four use populations (graphql_field and graphql_argument by named_type, graphql_implements, graphql_union_member) as one statement, honouring ReferenceContext.isIncludeDeclaration and listing every declaration site rather than the one a jump would pick. BindingReferences takes a cursor inside a directive argument and dispatches on the exhaustive Behavior switch, so a new binding arm forces a find-references decision at compile time; a @nodeId(typeName:) folds into the type population rather than standing up one of its own, so the same question has one answer from either side. The freshness question the spec split out from precision is settled the honest way: every position is the last capture’s, no buffer refresh, and the manual says so in its own words. The argument is that IntraSchemaDefinitions prefers the open buffer because for one declaration the buffer holds the whole answer, while here it holds only the fraction that happens to be open, and a list half live and half captured is fresher in a way nobody can predict. Both dispatch-matrix seams closed rather than left to review discipline: Reach now requires every surface to carry a verdict on every trigger, with no complement-filling convenience, so a constant added to LspSurface fails the build until all twenty-one rows have decided about it; and LspSurface states its own keying, so the cursor-versus-sweep guard derives its surface set instead of hand-listing one. Two things the implementation found that the plan had not: the class population is every positioned carrier of a class name (@service, @externalField, @enum, @sourceRow, the two condition families and the three reference-step families), not the three the spec listed, and the column arm needs the parent-binding fallback its jump twin has. Slice C (value-level positions in capture, so a result lands on film rather than on @table(name: "film")) is deliberately unshipped, as are field-name and enum-value subjects, the latter now deferred visibly: a cursor on a field declaration name answers empty and a test pins it. Reviewed at In Review -> Done against the tree rather than the plan, which is how the two stale slice SHAs in the plan body and one javadoc sentence misdescribing the class arm’s match as case-insensitive were caught; the match is exact, correctly, since a Java identifier is case-sensitive, and the sentence is corrected in this commit.

  • R749 (bb0e9d47 the alias namespace and the type-scoped fold, 0cf1b65b the census plus the fan_target fixture and the doc promise, 64a38143 the round-2 argument pin on a fixture that can express it, 981daf28 the codegen schema-version bump; filed 743cb893, Backlog → Spec 91e3ad12, spec revisions 8f1a379d + 57caa525 + 12ad1b9f + 2728e5d9 + 4e49fc9e, Spec → Ready a2f9629e, Ready → In Progress c2a5d6ef, In Progress → In Review e233bb1a, In Review → Ready 153596e3, Ready → In Progress b47323741, round 2 recorded 6e6868c2, In Progress → In Review 6478ffe5): two participants of one single-table discriminated interface declaring a same-named field over different join paths minted one __rk_ alias, and because an aliased jOOQ field compares equal on its alias alone, the fold’s LinkedHashSet dropped the second term and the losing type’s rows read the winner’s column with no diagnostic, no error and no warning. The namespace verdict is now a model fact: AliasOwner (Shared / QualifiedBy) is stamped once at capture in FieldBuilder.aliasOwnerOf off the index TypeBuilder’s existing discriminated-interface scan builds, and both halves of an aliased term compose their prefix from that one value through `ReservedAliases.resultKeyPrefix, so a participant-declared name is owned by the participant type (two distinct terms, each fetcher reading its own) while an interface-declared name is owned by the interface (one identical alias in every arm, the agreeing case still collapsing to one term). The $ delimiter makes the composition injective, GraphQL names admitting no $. Beside it the fold hands each branch a selection restricted to the names whose alias that participant qualifies, which is what makes "a shared alias requires a shared occurrence set" hold by construction: the polarity is the complement of the spec’s, deliberately, because restricting the shared names instead would have re-created this very defect for the bare aliases a spliced nesting unit contributes. The two populations qualification cannot reach are build errors rather than silent drops, as deferred rejections drained by the validator, and the author-facing split is stated in code-generation-triggers.adoc: declaration-time divergence on an interface-declared name is a build error, query-time divergence on one stays a runtime client error, and a participant-declared name resolves per type. Reviewed at In Review -> Done with the emitted tree read rather than the argument: the fold emits Set.of("marks", "target"), each participant’s marks arm correlates on its own FK under __rk_Fan<Alpha|Beta>$ while details keeps the shared __rk_FanItem$, and every fetcher reads back the prefix its own arm wrote.

  • Reverted: the routine-write hop pairing’s expression-shaped join keys (c702da22c, reverted in this commit): an unnumbered change that repaired nothing, shipped three and a half hours after R819 had closed the regression it was chasing. RoutineWriteFacts.hopPairs joined intent_foreign_key_column_pair on a CASE over fk_on_from, and the mechanism the commit named is real: intent_field_chain_node is recursive and carries four window functions, so no outer predicate prunes it, and fact-model.adoc prices that shape under its rule about a derived relation joined on an expression. What was missing is a measurement. The stated evidence is a thread dump of a stalled generate, which store-performance names as the failure it exists to prevent and which this store has already yielded two retracted conclusions from, and the commit’s own message records that no end-to-end sakila generate completed on the tree it shipped from. A same-fixture control settles it: RoutineWriteCommands.produce over the sakila example’s own schema, three runs per shape against one capture, costs 487, 211 and 248 ms with the projection and 492, 216 and 195 ms without it, five rows either way. The stall it described was R819’s carrier regression rather than this join’s, which is why the five consecutive trunk runs carrying R819 and not this commit finished in 13.6 to 19.8 minutes, one of them (9c3113350) adding the statement-count pin over this very producer. The hazard the commit correctly identified is left where R682’s plan and R765 already put it. The hops reading beside it keeps its own CASE and always could: it joins sql_constraint, a base table rather than a view, so the rule does not reach it.

  • Discarded: Break the build when the reactor exceeds a wall-clock budget (reactor-wall-clock-budget, R824): superseded by R733 (build-wall-clock-guardrail), which has held the same question in Backlog since 2026-08-19 and which R824 did not cite. The duplication is not only of subject. R733 already settles the shape R824 proposes: under the -T 1C that CI uses, the reactor’s wall clock and its critical path are the same number, mvnd’s own scheduler putting the path at 340s against a 340s wall clock with a forced-sequential build at 339s, so a total-suite budget cannot distinguish a build that got slower from one that got wider, and R733 carries that measurement as the argument against exactly that option and for a per-class ceiling read from the Surefire reports. R824 proposed the total-suite budget on the strength of one incident. What R824 held that R733 does not is the incident itself, and it is not lost: R682’s slice-one reflection carries the eleven consecutive green builds at four times normal wall clock as the guardrail’s motivating case, and R819’s entry below carries the recovery. The structural companion R824 named, a meta-test asserting that every relation a plan-tier facts class names is either registered in meta_materialize or explicitly exempted, was already out of scope in its own body and wants an item of its own; intent_mutation_routine_seat is still an unregistered view on the generate path and would be its first finding. The ID stays a gap, per the never-reused rule. Re-open trigger: none; if R733 is ever descoped to the derived-read slices alone, the guardrail becomes a new item rather than a resumption of this one.

  • R815 (045ef6ca9 the delivery, 0d1e6a64f + 490674b7d the corrections; specced d972352-reviewed and 5435635-revised, Spec → Ready 0745ac4dc, Ready → In Progress 88d7323da, In Review 3c3ac9384; Done-gate review in this commit): the seven materialized targets were the only tables in a 152-table fact schema with no key on them, and that, rather than materializing itself, is what made three registrations cost some other relation’s read more. The correction the measurement forced is where the cost lands: not on the reader’s own predicate, which for a WITH RECURSIVE reader carrying window functions never pruned the rule either (its coordinate-filtered read and its whole-relation read cost the same scans in both shapes, to the scan), but inside the derivation, on the join a registered view performs against the target once per driving row or once per iteration. So the lever is underneath the reader and no reader was restructured. Five indexes shipped, each on the columns a named reader joins its target on and each naming that reader in a COMMENT ON INDEX, chosen by measuring every candidate shape against every view whose derivation reaches the target with statistics held constant on both sides: the largest set under which all 36 readers improve and none gets dearer. The plan’s expectation inverted, intent_argmapping_pair and intent_node_id_decode_hop_column both earning one and intent_resolved_type_binding and intent_field_column_scope going to the roster instead, the first of those declining the single largest total gain available because no shape of it spares every reader. Nothing is UNIQUE, and not for the nullability reason the spec anticipated: the columns a reader joins are a coordinate prefix and never the grain, so an index here serves readers or it serves nothing. Statistics are the other half, H2 gathering none at this scale; Materializations.analyse supplies them, best-effort against a database refusal and returning a count so the swallow cannot mean unobserved. It could not go where task 2 put it, and for correctness rather than cost: H2 commits the current transaction as a side effect of ANALYZE, verified by inserting, analysing and rolling back to find the row still there, so refreshAll analyses inline and FactCapture analyses after its transaction closes. Both refill paths are covered. MaterializeRegistryGateTest grows three gates, index-or-rostered-with-a-reason asserted by equality on HAND_WRITTEN’s model, every index naming its reader, and analysing a healthy store reaching every registration; all three of `DerivedReadCostTest’s large non-monotonic pairs are gone with the registrations they were charged to unchanged, its fixture now populates the seventh target (inventoryForFilm, an argument whose scope table declares the one key the node type’s table needs), and its budget floor moves from 2 s to 30 s so an equality-pinned exhausted set stops depending on machine load. `SurfaceScanCountTest moved only its per-declaration pair, 42/41 to 40/39, and the class now records why: at three types every target holds a handful of rows, so the ceilings are blind to this lever by fixture size and not by its being imaginary. fact-model.adoc states the conditional and the corrected mechanism. Independent-session In Review → Done review: full mvnd install -Plocal-db green; the one red run before it was GraphQLQueryTest.splitTableField_conditionJoin_returnsActorsPerFilm observing a film_actor row a concurrent mutation test transiently seeds, diagnosed to DmlBulkMutationsExecutionTest’s `(actor 2, film 3) pair and filed as R825, unrelated to this item and to the H2 fact store entirely. No code-string assertions on generated method bodies; retirement sweep skipped, the item retiring no vocabulary; user-facing-doc check clean, the only docs/ change being the contributor-facing fact-model.adoc.

  • R819 (1f8505076 + 5e78faeb2 the delivery, fd1c4120a the schema-gate fixture; filed 5f0b9655c, spec e40993854, Spec → Ready 3e545b2a0, In Review 85c8d694d; Done-gate review in this commit): the carrier-family read-cost regression beside R682 slice one is repaired by two registrations and one body restructure, no answer changing anywhere. intent_errors_field and intent_carrier_data_field move onto the capture cadence (rules kept in _live views, tables under the canonical names, registry reasons stating the measurements), and the carrier body’s windowed data_channel CTE is named once instead of four times with its three disqualification arms standing on the base tables, which alone took the sakila carrier read from 6.6 s to 173 ms; post-registration the family reads the floor (seat 17 ms from 43 s, hop 53 ms from 10 s, error channel 12 ms from 5 s, carrier-free refresh 12 ms). The intent_poly_member slice was measured and declined on its own control. Both new targets went to the no-index roster on measurement; the read-cost fixture grows a routine-carrier cluster per unit so the gate prices the family (47 readers, 107 cells), and the four named-type pairs it flagged are pinned with their lever filed as R820. Census prose now cites the register without a number.

  • R800 (42614bd the delivery; filed at Spec aabf6c8, spec reviews 86ae74d + 5e38d68 + 9c51600, spec revisions 12af0dc + 3516a32 + 587c9f7 + 099054b, Spec → Ready f3ec0af; Done-gate review in this commit): a family page in the generated schema reference now presents its family before defending its name, and how two families meet is declared data rather than prose about the store. meta_family gains an introduction column, one plain-language paragraph per family, its column comment carrying the discriminator that an introduction names no relation and no other family because the two rosters carry those and are gated. meta_family_headline authors the relations a reader meets first, membership only: the family comes from the census by join and the blurb from the relation’s own comment, so nothing is authored twice. meta_family_bridge declares the sanctioned normalization crossings, four rows, membership being ownership of a rule spelled in the relation’s own body plus a meeting between two families' vocabularies. meta_relation_reference derives the declared key edges out of INFORMATION_SCHEMA at constraint grain, both ends carrying their census family. StoreCatalog reads the three new relations verbatim, GrainSentence lifts a relation’s first sentence beside it because that convention is the store’s and not the renderer’s, and StoreProse becomes the one reader of the store’s prose corpus so the renderability gate and the drift guard cannot sweep different corpora. The family page is now introduction, "Where to start", "How this family meets the others" in two labeled parts, the charter under "Why the name is right", then the relations; index blurbs read as introductions. The declared crossings are presented as declarations and claim no exhaustiveness, closure against what the views actually read being the follow-up’s derivation. Tests: FamilyRosterGateTest closes both rosters against the observed schema in five gates (non-blank introductions; headlines resolving to a censused relation with ordinals dense from zero per family and at least one per family; bridges naming an observed relation, two rostered distinct families and a rule), GrainSentenceTest pins the extractor’s acceptance line in both directions against the corpus’s real dot hazards and sweeps every censused relation for a sentence that is a prefix of its comment and terminated, SchemaReferencePagesTest pins each renderer floor with its own negative case, and SchemaIdentifierDriftCheck gains the store’s own prose as a second corpus with a bare-token extractor and a vacuity floor, which surfaced four citations the schema does not declare, all fixed. Verified at the gate by a full mvn install -Plocal-db and by reading the rendered pages: all thirteen family pages carry the three new sections, and CI’s docs-deploy published them.

  • R793 (272ef13 the delivery, fc4535b the rework round; filed and specced with the roadmap sync 0e643f3, Spec → Ready 72f16f7, Ready → In Progress ed71195, In Progress → In Review f163ba7, In Review → Ready d033130 with a corroborating second gate at e9c39d0): the language server’s diagnostics drain issued one statement per graph and that statement did not finish inside the 30 s session budget, so a developer’s squiggles silently stopped tracking their schema. Diagnosed inside the database rather than by profiling the reactor, arm by arm: the drain’s own statement was captured off the production read and each of its 22 select-list arms timed alone against a sakila capture. The budget was hiding most of the defect it reported, the 31310 ms an earlier harness recorded being the time until the abort rather than the statement’s cost, which runs past seven minutes to completion. Two hypotheses died and both refutations changed the plan. The static multiplicity ranking predicted nothing, intent_type_backing_seed measuring 15 ms. And the leading hypothesis named the right mechanism on the wrong lever: the redirect arm’s census-driven join is the worst term, but the driving-side rewrite prescribed for DeclarationFacts was unnecessary here because the ruinous term sits one relation further down. intent_resolved_type_binding carries a COUNT(*) OVER, so no outer predicate prunes it and every naming pays a whole evaluation; filtered to one type the backing arm is 22 ms, filtered to a document’s types it is 1.9 s, and driven from the census it never finishes. Registering it collapsed the redirect, backing and slot arms to single-digit milliseconds and left a 9.5 s residual whose 63758 scans sat on sql_constraint_column, re-entered per row by the unresolved-path arm’s correlated NOT EXISTS against intent_field_column_scope; registering the scope too took one evaluation of intent_field_column_table from 131 s to 144 ms. Whole statement: 191 ms, against a 3 s interactive budget. No Java changed. Both meta_materialize rows carry the arithmetic in their reason column, the pair’s refresh is about 175 ms per capture, and registering the 131-second view itself was refused on exactly the refresh-cost ground the plan reserved for it. The pin is DiagnosticsStatementCountTest.theDrainsStatementStaysCollapsed, which captures the drain’s own statement and EXPLAIN ANALYZE`s it for a total scan-count ceiling, the clock-free currency `SurfaceScanCountTest had already established on trunk. Independent-session In Review → Done review across two rounds. Round 1 passed the delivery on correctness and sent it back on its own named evidence: the pin could not fail on the shape it guarded. Over the class’s four-type fixture the two shapes measure 658 and 1291 scans, so a 20000 ceiling, explained by a hundreds-of-thousands figure belonging to a sakila-scale catalog the tier does not stand up, sat fifteen times above the regression and passed on a tree with the fix entirely removed; a second concurrent gate reproduced both numbers by a different method and measured how far the fixture would have to grow to fix it properly. The rework takes that second repair: the case now stands up its own graph of forty table-bound types, where the collapsed shape totals 6924 scans against the unregistered shape’s 23983, and the ceiling is 15000. Round 2 reproduced both numbers exactly by reverting the two registrations from the DDL, rebuilding the model and re-running the case, which failed on 23983, and by lowering the ceiling on the restored tree to read 6924. Full reactor green under mvnd install -Plocal-db; no code-string assertions on generated method bodies; user-facing-doc check and retirement sweep both skipped, the item touching no docs/ file and retiring no vocabulary. The citations this item left in three sibling roadmap items were restated in the approval commit rather than repointed, its file being the one deleted here.

  • R803 (ed424f6 the delivery; filed at Spec aa9dad2, spec revisions be57ab3 + 0f6a090 + fb466c6, Spec → Ready 1ae37d3, Ready → In Progress 4948095, In Progress → In Review 24c0c4c): five columns in the fact schema serialized a collection into one scalar, and the discipline they failed is now written down and gated. The discriminator is atomicity plus key-dependence rather than a distaste for renders, which matters because the store legitimately holds renders: diagnostic.coordinate and the _upper folds pass, and intent_type_backing_conflict carries the whole lesson in one relation, candidates passing where class_names failed on the same key. The columns were not oversights and defended themselves in their own comments as canonical group keys, so the item had to answer that defence rather than assert past it; what settled it was a live wrong answer, DiagnosticFacets filtering directives with isNotDistinctFrom against the joined set, so an author asking for conflicts involving @service silently got only those whose entire set was exactly service. The MCP surface trades that dimension for a directive filter that asks membership by joining, and Filter sits beside Dimension rather than inside it because a finding carries several directives and a groupBy over a multi-valued key would multiply the rows the aggregate counts. The message fork resolved onto the post-capture arm on the schema header’s existing first reason: the render’s input is AuthoredClaim’s declaration order, which is not a captured fact of any graph, so no view can state it. `intent_authored_claim_rejection holds it, written at capture cadence, with kind and variant minted alongside from RejectionKind.of and RejectionFacts.classSpelling, which took both Rejection.* string literals out of the DDL and dissolved the Rejection.* half of R807. The load-bearing test is not one the spec asked for: theMintedRejectionMatchesTheViewRowForRowAndWordForWord pins the one-to-one the diagnostics inner join rests on, without which a missed mint would drop a diagnostic silently. CollectionValuedColumnGateTest denies the five constructs in the DDL’s statement regions, blanking -- comments and literal bodies in one pass so the file’s own prose about LISTAGG cannot fail a clean tree, and discloses in its javadoc the half it cannot cover: diagnostic.directory was a collection in a scalar with no aggregate anywhere, so the gate would not have caught it and the fact model’s prose is the enforcer for that half. R804 states the principle in the explanation articles.

  • R799 (10b7e96 the delivery; filed inside 480bb20, renumbered 61ffdf7, Backlog → Spec ff5b49a, Spec → Ready f8963a0, Ready → In Progress 70c8727, In Progress → In Review 3bd53a4): the inlay-hint read was found already repaired, and what shipped is the enforcer that would have noticed either the defect or the repair. The item was filed from a measurement of 13789 ms and 561746 row scans for one fifty-line inlay request over the sakila example’s 4222-line schema, which is four times INTERACTIVE_READ_BUDGET and therefore no annotations at all; re-measured on trunk a day later the same request cost 65 ms and 21065 scans, and a whole-file request producing all 450 hints cost 170 ms. The attribution is a same-fixture control rather than two runs believed: with the model DDL checked out from 272ef13^ and graphitron-model rebuilt, that window returns to 10205 ms and 561851 scans, so R793’s two meta_materialize registrations, made for the diagnostics drain and not for this surface, are the whole difference, and intent_resolved_type_binding’s `COUNT(*) OVER is the term every arm of the statement reaches. What survives the repair is the shape: the read is region-scoped in its questions and graph-scoped in its evaluation, so a ten-line window and the whole file scan the same rows, and over a grid of types against omitted-name @field sites the cost is linear in both dimensions with no cross term at about 30 scans per declaration. The finding that made the item worth keeping is that SurfaceScanCountTest, written to stop exactly this class of regression, passes green and unmodified against the shape that produced nothing: inlay cost 1544 scans at its own three-type fixture under a ceiling of 1800, and so did every other surface’s ceiling, including the drain’s. So INLAY_CEILING is now 800, placed between two measured shapes at 482 today and 1544 guarded, carrying in its javadoc the rule that a number whose whole value is discrimination cannot be raised on the strength of the current cost alone; and theInlayReadCostsABoundedAmountPerDeclaration states at two schema sizes what a fixed small fixture is structurally blind to, 42 then 41 scans per declaration today against 150 then 141 guarded, asserted as a level rather than a growth ratio because both shapes are flat and a ratio separates nothing. scaledSdl gained a sites dimension while its one-argument form stayed byte-identical, so the census test’s fixture is untouched, and scansFor and recording now take the store they measure, which is what makes a second surface’s assertion one more call rather than a second mechanism. No production code changed, and the item argues for that rather than eliding it: the read is inside its budget on the largest schema the project has, and the levers that would cut the constant are a registration whose refresh every capture pays and a rewrite of arms whose plans do not expand per driving row. The class javadoc now carries both the standing rule that a ceiling is finished when it has been seen to fail, not when it passes, and the named gap that the five other surfaces still have only the net. Independent-session In Review → Done review, mutation-tested rather than accepted for passing: on the reverted DDL both assertions fail at exactly their documented figures, 1544 against 800 and 150 against 80, with theCensusLookupDoesNotTrackTheSchemasSize still green, and all four per-declaration figures were read back individually by lowering each ceiling to 1. Full reactor green under mvnd install -Plocal-db. User-facing-doc check skips, the item touching no docs/ file; the item retired no vocabulary.

  • R794 (60397ef the delivery; filed f8ddef1, Backlog → Spec 78f94d7, spec reviews d812813 + 278334b, spec revisions 077c173 + 04f89aa, Spec → Ready e8d8fa2, Ready → In Progress f13e6b7): detaching an editor from graphitron:dev no longer fills the dev console with stack traces for answers written onto the socket the teardown just closed. LauncherFactory in graphitron-lsp builds the lsp4j launcher both transports use, the stdio Launcher main and DevServer.serve, so the connection policy is configured once and a third transport inherits it; the policy is quietOnStreamClosed, applied through Launcher.Builder.wrapMessages so it covers both message directions, consulting JsonRpcException.indicatesStreamClosed on a consume failure, dropping the message at slf4j debug on a true verdict and rethrowing everything else so a framing or serialisation failure stays loud. wrapMessages rather than setExceptionHandler because a handler is never consulted for a notification, which is what a publishDiagnostics push to a departed client is: lsp4j catches that write failure itself and logs it at INFO carrying the throwable, and JUL’s default console handler prints a stack trace for any record carrying one, whatever its level, so a SEVERE-only fix or assertion would have left half the noise standing. Teardown also stops leaving the dead connection reachable: Workspace’s recalculate slot became an `AtomicReference with clearRecalculateListener as setRecalculateListener’s compare-and-clear inverse, the service holds its listener `Runnable in a field created once and surrenders that same instance from a new disconnect() chain (GraphitronTextDocumentService to GraphitronLanguageServer to DevServer.serve’s `finally, before the executor shutdown), and the compare is on identity because every evaluation of a method reference is a fresh object, so a clear handed a second evaluation would silently never match while an unconditional clear would silently stop diagnostics for a reconnect that already took the slot. The RejectedExecutionException absorption stays as the guard for the residual race of a mutation that read the slot before the clear. The spec’s history is the process working: each of two review rounds killed a load-bearing factual claim checked against the tree or the 0.24.0 bytecode rather than accepted (round 1: a failed notification write never throws into the recalculating thread, RemoteEndpoint.notify catching it itself; round 2: the drain executor from the item shipped in between already absorbs the stale window’s work, leaving reachability as the second deliverable’s only real cost, and the compare-and-clear needed an identity the plan had not named). Independent-session In Review → Done review, mutation-tested at the gate rather than accepted for passing: with wrapMessages removed both end-to-end pins fail on exactly the two records the item removes, SEVERE: Internal error: ... SocketException: Socket closed and INFO: Failed to send notification message., the response pin five of five in isolation with one observed pass in a full-class run where the record published after the assertion read, a narrow window worth knowing about while the class still went red via the notification case; surrendering a fresh method reference instead of the field fails ConnectionTeardownTest’s positive case. Full reactor green under `mvnd install -Plocal-db, LauncherFactoryTest and ConnectionTeardownTest re-run green on the restored tree. Retirement sweep on "listener slot" found one survivor beyond the three prose sites the item named and rewrote: DiagnosticsDrainThreadingTest’s javadoc still stated the slot is never cleared, and was rewritten to the residual race in the approval commit; the R796 changelog entry keeps the old claim as the historical record it is. User-facing-doc check skips, the item touching no `docs/ file.

  • R728 (8284400 the named-parameter carrier and the bean-member deferral, c79f4fd the bare spelling, 8df0217 the scope’s second rung; the relations and their stages at 4548c98f, 705f96b6, e1ad3ae3..375943a9, e578ef47..e9dc149f, 417298d3, cb502a21, 9c801d16, 12dabb11, 2e46090a, d0f6358c, 6d70b6a4, 8b819323, 270cc6a3, 2d7223b4, 7287451e, 5335f0ec; In Review → Ready 03267bc, 80c4007, 3dc5ca4): @nodeId encode and decode become relations in the fact store, and an instruction the generator drops fails the build instead of handing a consumer the wire format. The instruction population states all three authored forms over three sites and resolves each to a node type through five disjoint bases; two resolution relations answer the two directions, four decode destinations and two encode sources; intent_node_id_decode_defect mints the two verdicts the walk cannot reach before capture, key arity exceeding a single-valued slot and a key column whose type disagrees with the slot’s, projected into located build errors by NodeIdDecodeDefects as a fourth component on StoreDetections. The junction chain stops being a rejection and becomes absent local columns reaching the hop-general EXISTS, pinned by execution-tier row counts against PostgreSQL rather than by an argument that the EXISTS multiplies nothing. The read family gains its encode at the accessor, by-name, typed-column and @error arms. Site 4a’s refusal separates its two causes and the manual page gains the reverse hop that was authorable all along with nothing telling an author so.

    Four independent-session In Review -> Done rounds, and what they found is the item's real shape. Round one refused on both gate questions: at the named-parameter carrier the item newly judged, no schema built at all. The two signatures the store's own refusals prescribe as remedies, a parameter of the key column's type and one of the node table's generated record, both drew `+WireCoercionError.Assignability+`, whose message offers "route the value through a converting scalar / `+@nodeId+` decode" on a schema that had written one, while the parameter typed as the wire format classified to a base64 pass-through, which is the defect the item exists to close. The stand-aside had been wired into `+RoutineDirectiveResolver+` only. Site 2, a `+@nodeId+` scalar on a bean-backed input, reached the same pass-through with no rejection at any tier. Both were invisible to the delivered evidence because the two cases covering them asserted only that a detection reported nothing, which passes equally well against a red build; that is why every case in `+NodeIdProducerSlotDecodePipelineTest+` now asserts the schema *builds* and reads the slot's own transform. Round two found the same fall-through surviving in the bare spelling of the directive that all six new cases wrote explicitly, with the walk and the store disagreeing at identical rows. Round three found the inference the second rework landed sharing one of `+intent_argument_scope_table+`'s two rungs while three places claimed it shared both, so a bare `+@nodeId+` on a service-backed delete was refused for want of a table the store had already resolved through `+MUTATION_TABLE+`; the two javadocs asserting that parity were the part that mattered, standing in main sources where they outlive this item. Round four approved: the delete surface classifies to `+decodeFilm+` on `+film_id+`, rung precedence matches the relation's own ranking, the neither-rung refusal names both ways in, and the corrections landed in all three places. Full reactor green under `+mvnd install -Plocal-db+` on the exact reviewed tree, 14 modules with zero failures, compilation and execution tiers and the docs render included; no code-string assertions on generated method bodies in any delivered test; user-facing-doc check clean over three touched `+.adoc+` files; retirement sweep clean across javadoc, the manual and the architecture docs, the surviving "identity-carrying" occurrences being the concept name for the chain shape rather than the retired gate, requirement framing and rejection text. R691, the Backlog tombstone recording that this item had absorbed the manual page's overstated single-direct-FK no-JOIN claim, deletes at this gate on its own terms: the page now names both single-hop shapes and states the discriminator, that one direct foreign key is not a promise of a single-table predicate and the landing is. The resolver reading these rows stays with R682, stated in the item rather than implied: `+NodeIdLeafResolver+` runs before the rows exist, so this item takes it as far as spelling the relation's rule rather than a second one.
  • R791 (b4a8ecc the delivery; filed and Backlog → Spec 1ae8063): a friendly explanation article on the fact store’s naming discipline. docs/architecture/explanation/naming-the-row.adoc teaches "name the row, not the question" in simple language: the store as a database of sentences, grain and fact glossed via a spreadsheet-of-sales comparison, a pantry metaphor (label the jar by its contents, never by your plans for it), the type-to-table binding as the worked example over the real relations (graphitron_table, sql_table, intent_spelled_table, intent_bound_table), benefits stated as consequences rather than virtues, and an honest costs section naming the relation census, the ungated nature of the one-sentence check, the sanctioned diagnostic exemption and the disclosed inversions. Linked from the explanation index and from fact-model.adoc’s preamble; every relation cited was verified to exist against a store booted from the DDL and read through `INFORMATION_SCHEMA. Landed outside the reviewer gates on the user’s explicit call; the workflow was waived for this item, so there is no In Review → Done review to record.

  • R685 (62343d2 the census cut, the classified list and the enforcer, 2d8ad9a the manual sweep at the Done gate; filed 1e3c082, Spec → Ready withheld f6ce1a9, author revision 8e3cead, Spec → Ready da011cf, Ready → In Progress 4b78ba3, In Progress → In Review 748743b, In Review → Ready 87f44bc, Ready → In Progress 1ba1bea, In Progress → In Review 09a2458): the class census stops at the consumer’s declared classpath, and naming past it fails the build. It was taken over the whole transitive closure: 169 classpath entries for graphitron-sakila-example, of which 142 jars arrived behind the 15 the pom declares, carrying 87% of everything the fact store held and 92% of each scan’s read time. Decompression rather than parsing is the bill (13 ms to list every jar’s entries against 486 ms to inflate their .class bytes), which is what decided that the fix is a narrower entry list and not a shallower read; a per-class depth cut was rejected in the spec because completion has to answer before the author has written the name it would offer, and it composes with R762 rather than competing. The narrowing is not two lists. ClasspathEntry(path, origin, coordinate) carries the decision on the element with four origins, PROJECT / DECLARED / SIBLING / TRANSITIVE, produced once by AbstractRewriteMojo.resolveCompileClasspath (the earlier resolveClasspathRoots half is gone rather than left beside a narrower sibling), and every consumer projects from it: the codegen loader, javac and the dev execution loader take every entry, the census skips TRANSITIVE before opening it, so census subset-of loader is a derivation over one list instead of a promise about two that buildCodegenLoader’s own javadoc records as having already broken once. Directness reads `Artifact.getDependencyTrail(), measured populated on all 157 artifacts of a real build with a two-element trail identifying exactly the pom’s 15 declarations, with a groupId/artifactId/type/classifier join (never version, since dependencyManagement rewrites them) as the fallback for an unpopulated trail. The other half is that the narrowing is a rule and not just a performance cut, because an invariant exists only while something fails when it breaks: ClasspathNameability is a resource probe against the classified list, asked for a verdict per author-written class name. The predicate is deliberately not census absence, which the Spec gate caught and sent back: jvm_class filters nested, non-public and jOOQ-package classes, so an absence detection would reject a nested class in the consumer’s own module, reject every JDK class, and stay silent on SIBLING, the one arm whose existence the design argues for. Nor is it getProtectionDomain().getCodeSource(), which the first reviewer suggested and the revision declined with the reason: the codegen loader is parent-first over the plugin’s realm, so a correctly declared graphql-java class loads from the plugin and would be rejected by anything asking where a loaded class came from. Whether a kept entry carries the name is the rule itself, and it is exact where the census is filtered. SIBLING is rejected naming the module to declare, a transitive-only class is rejected naming the coordinate that carries it (the one place a transitive jar is opened, after a name has already failed and the build is failing anyway), a platform-loader class is nameable, and an empty classified list is inert, which is what keeps the whole unit tier alive. Enforcement sits at the author-written sites rather than at every Class.forName: @service, @condition and @externalField in ServiceCatalog, @scalarType through a new ScalarResolution.Rejected.UndeclaredClass arm, @error in TypeBuilder, and @sourceRow in SourceRowDirectiveResolver, the last two being sites the plan’s own enumeration had missed. Because an enumeration defended only by review is the thing that rots, CodegenClassForNameGuardTest fails the build on any Class.forName in graphitron main sources carrying neither a nameability: checked nor a nameability: exempt (<reason>) marker, pinned in both directions against fixture sources and floored on scanned files and detected sites so it cannot pass vacuously. The rule is a breaking change twice over and both are declared rather than left inside a doc edit: naming a transitive class now fails (the migration is one <dependency> block, and it is the undeclared-dependency antipattern maven-dependency-plugin:analyze exists to flag), and the <plugin><dependencies> route is withdrawn, on an argument the item had not originally made, that generated code references these classes so an artifact only the plugin can load already failed the consumer’s own javac at the first generated reference. What the enforcer changes is when they hear about it and what it says. The prose surfaces moved with the population: jvm_class’s table comment and `meta_family’s `jvm_ definition say declared classpath and name the new filter, the two Diagnostics arms state the census’s scope instead of the falsified "not found on the compile classpath" (the LSP holds no fact separating a typo from a real class in an undeclared jar, so the message deliberately leaves the cause open where the build can name it), and ClasspathSources.record states the new silence as design, a skipped entry producing no store_source row at all because provenance is a use-site fact and a jar direct for one module is transitive for another. Measured at implementation, with the prediction that missed recorded as measured per the item’s own rule: 169 entries to 27 exactly as predicted, the scan 672 ms and 11,704 classes to 281 ms and 4,524, the workspace census 424,344 rows and 796 MB to 222,448 and 446 MB, and the write side 12.8 s to 9.2 s of jvm_ statement time per build, 28% rather than the predicted half, with both caveats stated (the narrow runs still scanned wide partitions a preceding run left behind, and the member-level merges R762 targets carry about 5.0 of the remaining 9.2 s, which is where R762’s half lives). Independent-session In Review → Done review, two cycles. The first sent it back on the Retirement sweep rather than on the code: the item declared "has to be on the plugin’s classpath, not the consumer module’s compile classpath" retired and fixed it in external-code.adoc, the one file the first-client check named, but the claim survived in five more manual pages across nine sites, two of them numbered recipe steps that would have produced the build failure this item’s own enforcer emits at @condition and @externalField, and three the per-directive reference entries for directives external-code.adoc itself enumerates as sharing these mechanics. The manual stated the rule and its opposite, at the surfaces a consumer reaches first. The rework brought all nine onto the one rule plus a tenth lead-in found by re-running the sweep, pointing at external-code.adoc#make-the-class-nameable rather than restating it, and withdrew mojo-configuration.adoc’s surviving "rare legitimate case" for `<plugin><dependencies> version pinning; custom-scalars.adoc was already correct and stayed untouched. Second cycle approved on both gate questions: full reactor green under mvnd install -Plocal-db on the exact reviewed tree, the new transitive-not-nameable invoker IT failing a real Maven build naming org.jooq:jooq and asserting nothing was generated, check-adoc-xrefs resolving all 212 cross-file anchored references so the five new xrefs are build-verified, no code-string assertions on generated method bodies, retirement sweep clean across javadoc, DDL, the manual and the architecture docs (the sole surviving occurrence of a retired string being a doesNotContain guard in DiagnosticsTest), and the user-facing-doc check clean over six touched .adoc files. Two non-blocking notes recorded at the gate: classifyElement adds a fifth arm the plan does not enumerate, an unattributable path classified DECLARED with no coordinate, which fails open, is argued in the javadoc and is pinned by ClasspathClassificationDecodeTest.anUnattributablePathStaysInTheCensus; and RecordBindingResolver’s author-written `className sites are marked exempt rather than checked, because ServiceCatalog gates the same @service / @externalField names before that observation pass runs, which is the better placement than the double gate the plan described. R620’s duplicate scan per dev-loop pass and storing entry provenance as a fact stay out of scope, the latter with its reason: an origin column on the store-global, definition-keyed store_source would be silently wrong the first time two modules in one reactor disagree.

  • R787 (5dc2452 the ordering change, 7c3a305 the coverage judgment, b113aac the symmetry correction; filed e989a82, Backlog → Spec with consult findings 8d94440, Spec → Ready 31bf3a4, Ready → In Progress 6697f47): the documented session flow rebases onto trunk before the verification build instead of discovering divergence at publish time. Sessions work trunk-based and many land commits concurrently, so trunk usually moves while a session works; the old flow (sync -> work + commit -> push own branch -> fast-forward trunk) put the only sync at the ends, so an agent built, invoked publish, learned trunk had moved, and had to rebuild, because a post-build rebase leaves the verification covering a tree that is not the one being pushed. Every trunk movement during a session cost a second full build and the agents were behaving correctly when they paid it. The fix is ordering, not mechanism: the flow now reads sync -> work + commit -> rebase on trunk -> verification build -> push own branch -> fast-forward trunk, which also names a build step the old flow left entirely implicit, and publish-time divergence shrinks from the routine case to the race where trunk moved during the build itself. "Verification build" gets one definition, at the top of CLAUDE.md "Building and testing", absorbing the -pl preference sentence that had lived at the end of "Common commands" plus the never-push-uncovered rule; the session flow, the "Common commands" sentence and the publish skill all reference that definition rather than restating it, three hand-maintained spellings of one obligation being the drift shape the development principles warn about. The fallback command block encodes the new order, the sentence advertising publish reframes its divergence pre-check as the mid-build-race backstop rather than where the rebase normally happens, and the rebase step points at .claude/web-environment.md because a mandatory rebase makes the documented init.sql jOOQ-catalog cascade a routine encounter inside the canonical flow rather than a build failure that reads like the agent’s own diff. .claude/skills/publish/SKILL.md states the caller expectation above step 1 and rewords its step 2 stop to name the situation and the recovery; the stop itself and every hard rule (fast-forward-only trunk, no force-push) are untouched. The item corrected itself in flight, which is the part worth keeping: the first live run of the new flow hit the shipped absolutism, which invalidated a verification build unconditionally on any post-build rebase and so forced a full rebuild when the mid-build movement was another item’s roadmap front-matter that could not interact with the change being pushed, reintroducing exactly the churn the item existed to remove. The rule became a coverage judgment instead, stated once beside the definition: every commit already on trunk passed its own verification build, so a rebase bringing in only plainly non-interacting changes (disjoint files, no shared build surface) lets the two green builds jointly cover the merged tree, with a cheap targeted check where one exists for the single touching point (a mvn -pl roadmap-tool exec:java -q regenerate after concurrent roadmap edits) instead of a full rebuild. A further correction made the criterion symmetric: the first wording treated "the incoming commits touch code" as an unconditional rebuild trigger, which is asymmetric and wrong, incoming generator code against a markdown-only commit interacting exactly as little as verified metadata against a code change, so the trigger is now the read relationship (either side touching build configuration or anything the other side reads, or any doubt) which is the criterion actually doing the work and which correctly catches the item’s own worked example, roadmap markdown being read by the roadmap-tool gates. The invariant that matters survives intact: a new commit of your own always needs a verification build that covers it. roadmap/workflow.adoc and the roadmap, srp and classified-corpus skills were audited and left unchanged, each for a stated reason rather than a blanket claim: workflow.adoc’s before-transition sync already sits upstream of any build the session runs, roadmap and srp already fetch and rebase before acting, and classified-corpus is the one documented verify-then-publish sequence but verifies with scoped -pl :graphitron -am test rather than the full install, so a publish-time rebase there costs a cheap re-run. Deliberately no enforcer: the never-push-uncovered rule lands as prose, and the deferral is filed rather than conditional as R788, which holds the marker-file check publish’s step 2 cannot substitute for, trunk divergence and "did the build cover HEAD" being different predicates. Independent-session In Review → Done review, approved on both gate questions. Full reactor green under mvn install -Plocal-db on the exact reviewed tree (BUILD SUCCESS, 14 modules, 6222 tests across seven module totals with zero failures and zero errors, check-adoc-tables / check-transient-citations / check-module-enumeration all reporting clean, the citation guard being the one that covers this item’s CLAUDE.md edit); no code-string assertions on generated method bodies, the item delivering no tests; user-facing-doc check and retirement sweep both skip, the item touching no docs/ file and retiring no vocabulary. Completeness was held to what a docs-only workflow item can actually show rather than to the build: each acceptance criterion was read against the delivered text at all four surfaces, and the audit the spec asserts as "confirmed, no change" was independently re-run rather than taken on report. The stronger evidence is that the flow has now been exercised twice for real, once by the implementer, whose live run produced the coverage-judgment revision, so the item was corrected by its own use, and once by the reviewing session, which followed the delivered front half and found trunk unmoved across a 6:14 build. Two non-blocking notes recorded at the gate: R788’s enforcer sketch predates the revision and would stop on a HEAD-vs-marker mismatch, which is precisely the carry-forward the judgment now blesses, so whoever specs it must reconcile the predicate (its own open questions reach the adjacent identical-tree rebase case but not the different-tree one); and the coverage judgment now carries one definition plus two short summaries at its referencing sites, the shape the Ready-time consult finding warned against for the verification-build definition, defensible because a bare cross-reference mid-decision serves the reader worse, but a future change to the criterion needs three edits.

  • R770 (7f78771e both deliverables; filed 4e0c419e, Backlog → Spec e9104229, spec reviews cceea3ef + c7aa2182 + 663d4c0e + 0339b737, Spec → Ready 5562ba7e): store slowness in a derived relation is now a database question with a documented first hour. docs/architecture/explanation/fact-model.adoc gained the general forms its measured H2 rules had been missing, each previously stated only per-registration or in an item body due for deletion: view inlining with no common-subexpression elimination and multiplicities compounding down a tree, a derived relation joined on an expression rather than a column evaluated once per driving row with its three controls, the two extraction cases distinguished so tidiness-extraction and column-projecting extraction do not read as contradicting each other, a recursive term re-evaluating its step’s input once per accumulated row, and the closing rule that what makes a relation expensive is being a view something reads many times rather than how a reader spells the read. Two lever paragraphs sit against the materialized-view ruling: captured fact before registration before rewrite, and materialize where the cost multiplies through, as deep as the sharing goes. .claude/skills/store-performance/SKILL.md carries the ungated half, posture and order of operations, on the split the item settled: the page is scanned by SchemaIdentifierDriftCheck and the skill is scanned by nothing, so the skill names exactly one relation and copies no per-relation number. The lever hierarchy’s reactor-pair figures were deliberately not written, both halves being deltas against a run their own source discarded, which would have put a retracted reactor total on a gated page that the skill’s posture section exists to warn against; the rung is written on its mechanism instead. Acceptance was a dry run against a real capture of the sakila example’s schema and catalog, which refuted its first subject, produced a live 151-second finding filed as R781, and returned two corrections to the skill’s own text.

  • R744 (e12adb06 the fix; filed 8626e5a6, Backlog → Spec 733e9515, spec revision f57a27f2, Spec → Ready 8a65e82c, Ready → In Progress 06f167eb, In Progress → In Review bb753b9e): two TenantScatterSubstrateTest tests waited for a recorded connect:<tenant> event before quarantining a tenant key, on the belief that the event meant the key’s worker had finished acquiring its connection. It does not: the fake connection records connect: from inside prepareStatement, which runs inside entryFor’s `computeIfAbsent, while the post-check that re-reads the quarantine set is still ahead of the worker. A quarantine landing in that window made the worker abort its own entry, the test’s events.clear() wiped the abort, and releaseAll found nothing left to abort, so the assertion failed on a missing abort: line, which is the shape CI reported. The emitted runtime was never at fault and is untouched: the post-check is what stops a late pin from leaking a live connection into a finished operation, and the contract under test (aborted, never closed under a possibly-live worker, never reused) holds in both interleavings. Only the clock was wrong. Both tests now park their workers on a latch counted down as the first statement of the per-tenant body, which a worker reaches only after entryFor has returned; from there the self-abort path is unreachable and releaseAll is provably the only remaining aborter. That is a proof from the control flow of perTenant.apply(dslFor(key)), not a widened timing margin. The event-polling loops and their deadline arithmetic are gone, the two comments asserting the old false claim are replaced, and the recording site carries a note that the connect event fires mid-pin. Verified with the item’s own mid-pin delay probe: pre-fix it fails both tests every run on the missing abort:A with abort:B surviving, post-fix 3/3 green with the probe still active, and 9/9 green without it. The straggler_releaseAllAbortsItsConnection_ test carries the same defect behind an 800ms margin and is left as is with the reason recorded, since its quarantine comes from a deadline inside scatter that no external latch can gate. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R750 (3fa5432 the mechanism, both guards and the model retype, e4f5498 the fixture conversion; filed 743cb89, Backlog → Spec 1411e45 with the body drafted at be48260, spec reviews 502f286 + 191c863, Spec → Ready cf593c9, Ready → In Progress 2121216, In Progress → In Review 55b27da): a @discriminate interface over a Postgres-enum discriminator column now generates SQL the database accepts. It previously failed on every query with operator does not exist: mpaa_rating = character varying, redacted to a bare reference id at the client, because the three discriminator comparison sites bound their @discriminator(value:) literals with no type information. Each literal now binds as DSL.val("<value>", <tableLocal>.<COL>.getDataType()), the generator’s standing typed-bind idiom, and jOOQ decides the rendering from the bind’s type: cast(? as "<schema>"."<enum_type>") on the enum arm, a plain ? on varchar. Authoring the cast ourselves was the rejected alternative, and the distinction is who decides and which statements pay. DiscriminatedTableFragments mints the qualified reference and the typed operand once each, all four sites calling the reference mint and PathFragments’ cross-table gate composing both rather than respelling the formula, so the comparison sites cannot drift from one another or from the projection. The `__discriminator__ routing projection stays deliberately untyped, an axis split now stated in the class javadoc: in SELECT the discriminator is a routing token the generated TypeResolver reads back as String, not a comparison operand. Two guards make the mechanism total. An unresolvable @discriminate(on:) is a classify-time AuthorError.UnknownName naming the table’s columns as candidates, replacing a silent raw-string fallback that only ever emitted code failing at query time, which makes TypeBuilder the invariant’s single enforcer and retires a stale comment pair where each end claimed the other enforced it. On a closed value domain (a jOOQ-generated enum) every @discriminator(value:) is checked against the enum’s literals, guarding the bind rather than adding a feature: DSL.val converts an unknown literal to null with no warning, so the check is what keeps the loud failure from becoming a silent empty result. EnumMappingResolver.checkEnumConstants’ inner comparison is lifted into `constantMismatches, parameterised on the one axis its two callers differ on: JAVA_NAME for the GraphQL-enum caller whose generated coercion calls valueOf, DATABASE_LITERAL for the directive-value caller, since mpaa_rating’s `'PG-13' literal is the constant PG_13; the literal read is reflective rather than a cast to org.jooq.EnumType, the class coming from the codegen loader. TableInterfaceType.discriminatorColumn and every carrier below it hold the resolved ColumnRef instead of re-collapsing to a bare SQL name, the render sites needing both spellings and Guard 1 making the slot total with no placeholder. Fixture conversion is the test strategy: content.content_type and jti_subject.subject_kind become Postgres enums (with the schema version bumped so an incremental build cannot reuse the stale catalog), reaching all four emission sites, while party_kind stays varchar as the control carrying both rendered forms. content_kind gains a third literal 'PODCAST' with no participant type, so the column’s value domain and the known-participant set are visibly different facts, which is the axis the closed-domain guard sits on. Independent-session In Review → Done review: full reactor green under mvnd install -Plocal-db (BUILD SUCCESS, 624 test classes, zero failures). Acceptance verified against the delivered tree rather than the green build: the baselines carry the cast at all four sites (in (cast(? as "public"."content_kind"), ...), the cross-table = cast(?...), the joined-detail ON "jti_subject"."subject_kind" = cast(? as "public"."subject_kind"), and the DML values (?, cast(?...))) with the __discriminator__ projection uncast; party_kind’s six statements are byte-identical at `= ? and in (?, ?); both guards reach a build failure through validateUnclassifiedType. No code-string assertions were added at the pipeline tier, and the render-tier ones the spec contracted are filed as R774 rather than held against the gate, since the SQL baselines pin the same four facts. User-facing-doc check clean over three touched .adoc files, including a polymorphic-types.adoc how-to sentence the spec did not enumerate. Retirement sweep skipped, no Retired vocabulary declared. One acceptance criterion is met by substitution and disclosed as such: the reproduction shape classifies with its enum ColumnRef in DiscriminatorColumnGuardPipelineTest but is not declared in the example schema, execution proof coming from the two converted families across every consumer path; a third discriminated family would exercise no comparison site they do not. The implementation also corrected its own spec on the read side and filed R754 for it: a String-typed SDL field over a jOOQ-enum column reads the Java constant name, not the database literal, because the emitted fetcher reads the typed jOOQ field and graphql-java serialises the constant with String.valueOf, and a jOOQ-generated enum carries its literal on getLiteral() without overriding toString(). That is invisible in the tree only because every seeded literal in both converted families is a valid Java identifier.

  • R755 (1c4028d; filed fe7d0d0, Spec revisions d4b4cb6 + f40e290 + 7480379, Spec → Ready 328f8de, In Progress → In Review 97f1bca, spec collapse 4b6bc75): The srp hand-off templates led with commit bookkeeping and gave the gate questions one sentence apiece, so reviewers came back with nits; roadmap/workflow.adoc now carries a What each gate decides paragraph naming the four questions the two guarded gates turn on, both templates open with those questions restated in reviewer voice and close with the bookkeeping, an explicit out-of-scope section routes naming/phrasing/formatting findings away from the verdict, and the In Review template points the reviewer at the delivered state of the tree with the commit series demoted to provenance. Mechanics dropped from roughly half of each template body to at most a third (27/81 Spec stage, 28/103 Implementation stage). Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R742 (4683e29 the mechanism with zero rows registered, 1f87258 the two registrations, b4316b1 the multiplicity report and the shared canonical tree and clean removal, 1c79305 the two follow-on re-measurements, 3506105 the Done-gate rework; filed and specced across 14b0126 + c5f8978 + 48ce5ce + 773bcd1, Spec → Ready 11888bb, spec corrections 6bfcadd, In Progress → In Review 26ca19a, readability pass 42edd94, In Review → Ready eb406d1): GeneratorDeterminismTest cost 229.0 seconds, a third of the whole build, and both halves of why were wrong. It ran the generator four times when its contract needed two, and each run cost 57 seconds of which 97 percent was the fact store re-evaluating its own derived relations. The lever that mattered was the second one, and the finding behind it generalises past this test: H2 inlines a view wherever it is named and eliminates no common subexpression, so one read of intent_argmapping_projection_defect expanded to 2066 relation instantiations, naming intent_argmapping_pair 55 times and intent_spelled_table 39 times inside a single statement, and scanning 2.57 million rows to return a handful of defects. Three measurements closed off the fixes that would otherwise look attractive: EXPLAIN costs 0.2s against 24.5s of execution so it is not compilation, the identical statement twice costs 24.52s then 24.53s so nothing memoises, and several relations in the tree carry window functions so no outer predicate prunes them and no rewrite restores pushdown. What ships is a registry rather than two bespoke reductions. meta_materialize pairs a view stating a rule with the table its rows are materialized into, and the mechanism’s whole trick is that the canonical name moves to the table: the rule keeps its text verbatim under a _live suffix, the sixteen other view bodies naming intent_argmapping_pair and the six naming intent_spelled_table are not edited at all, they simply stop hitting a view and start hitting a table, and so does each relation’s one Java reader. Materializations in graphitron-model refills every registered target inside the capture transaction, per graph where the target carries graph_name and whole where it does not, and that module placement is forced rather than preferred: the seeded fixture lives in its test sources and cannot reach graphitron, and the fixture has to call the same entry point production calls or the claim that the two boundaries cannot drift is empty. The doctrine moved with it, in three places the schema describes itself, from "materialise what a view cannot express" to a second admitted case, "materialise what a view expresses correctly and only too slowly", which is what the registry’s NOT NULL reason column exists to make a registration state. The mechanism landed first with zero rows registered, so it was reviewable on its design before any number depended on it. Measured on full green reactors: the class 229.0s → 15.46s on the two registrations, then 16.24s once the third contract clause was added, and FixtureWarningsGateTest 56.8s → 2.53s in the same build, which is the better number to carry away, being the same store reads under a different consumer. The run count deliberately stayed at four rather than falling to three: the shared canonical tree was built and does its job, and the run it freed was spent on clean removal, the third clause of the output contract, which GeneratorDeterminismTest’s javadoc had claimed for a class that tested two. At cross-cutting breadth that new case immediately found that the generator emits into four subpackages its sweep never visits, so a unit the schema stops calling for is left behind in any of them; that is filed as R756 and the case deliberately asserts nothing about those four, so the fix will read as a fix rather than as a regression. Thirteen `graphitron-model test classes broke on the first registration and none of them was asserting anything that changed: they were coupled to when a derivation runs rather than to what it returns, which named a missing concept rather than thirteen bugs. SeededStore modelled capture and query and had no derivation stratum, because under derive-on-read that stratum is implicit and free; it has one now, calling the same materializer everything else calls, so no test names a materialized relation and a fourth registration costs the fixture nothing. Two interactions the spec’s gate list did not predict were found by the build: StoreRefresh.wholesale() is written in exemption polarity and was emptying the registry on every warm capture with nothing to refill it, the rows being authored DDL, so the meta_ family is now exempt for a reason about authorship rather than cadence; and the DDL file’s first INSERT needed the boot to commit, schema statements committing implicitly where ordinary DML does not. Two further reductions were re-measured rather than done, which is what the item asked of them, and both declined on the numbers: flattening the defect view’s six-arm union now buys 22 percent of a store read that is no longer the dominant cost of a four-second run, and merging the duplicate read of intent_resolved_node_key_projection fell from 8.1s per run to 0.49s, which no longer pays for the wide intermediate row it would introduce across two classes. The multiplicity metric became a roadmap-tool step in the family of AdocXrefAnchorCheck, reporting rather than gating because it counts textual references without knowing which arms a predicate prunes, so a ceiling tight enough to be useful would also be wrong; it carries its two parsing mistakes as tests, the unstripped -- line comments that attributed prose section headers to the preceding view and inflated a total by 83, and the literal pattern that backtracks catastrophically on a 672 KB file. The gate ran two cycles and the first sent it back over a test rather than over production. FactSchemaGateTest.everyMaterializedTargetEqualsItsRule asserted nothing: that class’s fixture declares no argMapping-bearing directive and the capture attached no jOOQ catalog, so both registered relations produced zero rows and every assertion compared an empty list to an empty list. It was proved by mutation rather than by inference, an unscoped DELETE in refreshPartition leaving all twenty cases green, which is exactly the failure the case’s javadoc claims to catch and exactly the defect the item’s own clean-removal deliverable was filed to fix elsewhere. The rework gives the case a fixture written for it and a catalog to resolve against, populates both graphs because an empty sibling partition cannot distinguish "left the sibling alone" from "there was nothing to disturb", and asserts non-emptiness per registration before equality so a later registration this fixture leaves empty fails here rather than quietly contributing nothing. Second-cycle review, independent of the implementing session: full reactor green under mvn install -Plocal-db, 14/14 modules, no code-string assertions on generated method bodies, no docs/ changes for the user-facing-doc check and no retired vocabulary to sweep. The fix was verified by re-running the exposing mutation rather than taken on report, and by a second mutation withholding one target’s refill; the first now fails on the sibling-partition assertion and the second on the non-emptiness guard, and the case’s row counts were printed to confirm it runs over real rows. Also verified rather than assumed: MaterializeRegistryGateTest’s four structural cases are non-vacuous, including the no-ordering-needed claim by transitive closure over the engine’s own view bodies, and registration is invisible to consumers as claimed, `DevMojo being the only store-open outside FactCapture and refreshing on open so a warm start that skipped capture cannot serve the language server or MCP stale rows. Four non-blocking observations were left rather than folded into the rework diff (the twenty mechanical _live column comments the comment gate forces, readAll surviving only for an isNotEmpty() check, graphKeyed re-reading INFORMATION_SCHEMA per registration per refresh, and the class-total figure not reproducing exactly off the item’s 4 vCPU sandbox, which its own caveat covers); a fifth, that no authored page under docs/ mentions the registry and fact-model.adoc’s doctrine sentence still states the narrow version this item widened, is filed as R758. R746 carries the ordering the registry deliberately does not record, and its `depends-on cleared here.

  • R668 (4195667 the detection relations, 8c60ad3 routine writes emit from a command row, 1a91a24 the projection, a807f3b the openability rule in the grammar, a2607d8 the @condition arm and the type gate, 04a8cc8 the walk stops judging, 85a04bb the per-schema Tables capture, 2ef0b57 the producer reads the store, 25814a1 the Done-gate rework; filed d733f40, Backlog → Spec 813786e, fourteen spec revisions 301d084 + 73551be + cfd84eb + 1b4b8e6 + b267458 + 169b845 + 93b7942 + 3f3cea5 + 46b3ede + 9df5780 + ecf2324 + 9ab6700 + 3a8111b + 4f8cb41, attribution fix f79a112, In Progress → In Review 52d6e2d, In Review → Ready 444ac66, Ready → In Progress 84fda8a, In Review 2ccecdd; the Spec → Ready and first Ready → In Progress flips fall inside a squashed clone baseline and have no individually citable SHA): an argMapping path may open a @nodeId leaf into its node type’s key columns, which closes a silence rather than adding a convenience. Before this, pCustomerId: input.customerId on a @nodeId-carrying ID compiled clean and sent the base64 wire id to an INTEGER routine parameter, failing at runtime far from its cause; every one of the six rejections this lands is a spelling that used to build. The authoring rule is the one the dot already followed, extended rather than special-cased: a dot opens the thing at that position, an input object opens into its fields, and an ID carrying @nodeId(typeName:) opens into that type’s key columns, so input.customerId.customer_id is a path and customer_id is a key column of the node type rather than a field of any SDL type. The resolution is a chain in the fact store and not a scattering, which is the design claim the gate checked hardest: seven graphitron_*_arg_mapping_pair relations reduce to one intent_argmapping_pair with eight site values, then to what each segment bound, then to the leaf and whether it declares a @nodeId, then to a trailing segment naming a key column, then to a projection whose column type the parameter can take, each link reading only the one before it. The relation that had to be invented is intent_argmapping_bound_parameter_type, because the two populations answering "what Java type does this pair’s left side take" are unrelated: a @routine parameter’s type is a catalog fact and every other site’s is a classpath fact, so a reader had to switch on site to know which to ask, nobody did, and the type gate could not exist. That gate is a join predicate and not a check: an emitter reads the projection relation, so a pair whose types disagree is not a projection an emitter can see and there is no order of operations in which one is emitted and then rejected. It fires only where both operands are known and stands aside where either is not, so it strictly adds rejections and removes no emission; a parameter the classpath census cannot name (candidates > 1, a primitive int, a consumer compiled without -parameters) stays an unchecked projection rather than becoming a pair that is neither projection nor defect, which is the silence the item exists to close. intent_argmapping_projection_defect reads the same chain and states six verdicts with no precedence rule, its arms disjoint by construction over node_id_declared, trailing_segments and one existence test; the reviewer re-derived the disjointness pairwise rather than taking it, the load-bearing step being that node_type_ref IS NOT NULL implies node_id_declared in the leaf view, which is what keeps the type-mismatch arm from colliding with MISSING_TYPE_NAME or UNDECLARED_NODE_ID. Two coherent shapes defer rather than reject, and the distinction is the item’s own: a rejection says the author wrote something wrong, a deferral says they wrote something sensible that is not built, and the message points at the follow-up. A list-shaped node id and a projection at a site whose emitter does not read one both defer in Java rather than in SQL, because each is a fact about this codebase and not about the schema; EMITTING_SITES names the three wired sites and @service, the input-field @condition and the three reference-step sites each fail the build saying so. R735 carries the list-shaped emission and the ArgBindingMap.Result deferral arm. One deletion is the shape’s foundation: ArgBindingMap.of runs while the schema is being built, before capture, so it has no store to consult, and on reaching a segment it cannot resolve it now carries every remaining segment as an ordinary path step and decides nothing. A rule spelled there would be an earlier, unfalsifiable second copy of a view’s answer that won by rejecting first, and the two rejections a reader most expects the walk to own, UNDECLARED_NODE_ID and TRAILING_SEGMENTS_BEYOND_ONE, are in the view for exactly that reason; the one rejection that stays in the walk is a head naming no slot in scope, which is a question about the SDL surface the walk holds rather than about captured facts. Two supporting facts landed with it. sql_schema.tables_class_fqn is the per-schema generated Tables class, needed because a store-sourced TableRef cannot otherwise name a column constant, and it follows the rule its keys_class_fqn sibling established: resolved by loading the class off the codegen classpath, never by concatenating a configured package, the two diverging under multi-schema layouts. Its guard is not the agreement case that computes the expectation through the same resolver but perSchemaTablesClassesAreDistinct, which reads each schema’s own source package back out of the store, so a capture that derived one name from a single configured package would satisfy every single-schema assertion and fail that one. ColumnRef.decodeBindingType is the third way to get a columnType right, beside a sibling record’s decoded one and a live Class at the reflection boundary, and the one a store-sourced reader needs; it is array-safe because the captured name is the raw Class.getName() form, whose JVM descriptor is exactly what crashes ClassName.bestGuess, and a boolean-array column is an ordinary column in a consumer’s database rather than bad luck. The plan tier stopped reading the walk in the process: KeyProjectionCommands.produce takes no GraphitronSchema, command/KeyProjection carries only captured facts, and the plan-side seam ratchet went 140 to 139. The key list rides the command row whole rather than just the projected column, because the decode’s fromArray load is positional while the projected column is named, which is what makes a transposed composite projection unconstructable. The gate ran two cycles and the first one sent it back, over a test rather than over production: ArgmappingKeyProjectionEmissionPipelineTest pinned rendered method bodies as code strings in six of its seven cases, which the code-string ban forbids at every tier and which is review-enforced, making the Done gate the enforcement point. The rework re-expressed those cases through TypeSpecAssertions, the codebase’s own named remedy, which grew a projected-key section so the rendered spellings live in the one file that is allowed to know them, and resolved the condition glue structurally by its org.jooq.Condition return type rather than by a name scan. The behavioral half of "the decode precedes the write transaction" moved to the tier that owns behavior: GraphQLQueryTest.rentFilmPayloadProjected_badNodeIdIsARequestErrorAndCommitsNothing sends a malformed string and a well-formed id encoding another type, and asserts a request-level error, a null payload and no committed row, so a client error about an argument cannot be re-reported as a failure of the write. Second-cycle review, independent of both the implementing and the reworking session: full reactor green under mvn install -Plocal-db, the emission test carrying no rendered-body assertion (its one surviving contains reads a method-name list), user-facing-doc check clean over the nodeId.adoc and routine.adoc additions, and no retirement sweep owed. The claims table was re-verified against the tree rather than read off the previous gate’s note, all nine test-count claims matching what the run reported. Two residues are stated rather than rounded off. The emission cases are still body scans, now single-sourced behind typed helpers, which is the tradeoff TypeSpecAssertions exists to make and not an elimination of the scan; and invocationTakesProjectedRead admits other arguments between the invocation and the projected read where the old string required adjacency, a loosening judged not worth a third cycle. The one thing the gate carried forward before deleting the file is R626’s after-a-dot note, which said the "offer nothing rather than a misleading flat list" limitation is uniform across directives and stays uniform. That is now two arms with two different answers available: the input-object arm still needs a nested-input-field projection in the LSP snapshot, while the node-id arm is answerable from intent_resolved_node_key_column today and needs no new capture, graphitron_argument_node_id and graphitron_field_node_id already carrying node_type_ref at both coordinates. What is missing there is only the consumer, ArgMappingCompletions.rightCandidates returning nothing the moment a dot appears, which leaves the editor silent on a spelling the build now both accepts and rejects with a candidate list. The absorbed argmapping-completion-after-a-dot item’s file was already gone, so R626 is where that observation now lives.

  • Discarded: The assembled schema owns the composed SDL census (assembled-schema-owns-the-sdl-census, R714): absorbed into R743 (sdl-fact-gatherer-staged-pipeline) at the owner’s direction, applying the test the R678 discard states: a piece absorbs when it has no independent success criterion under the chosen strategy. R714 had one under an incremental strategy (swap the graphql_ payload’s transcription source inside the existing one-pass capture, everything else unchanged). The owner then decided the SDL gatherer restructure lands as a big bang, and under that strategy R714’s deliverable is exactly R743’s stage 4: there is no way to implement the full-schema traversal that is not R714, and doing R714 first would build it into the one-pass capture the restructure throws away. Nothing else depended on it (R713 references the composed-payload question but declares no dependency and stays downstream of R743 on the same terms). Its analysis travels rather than dying: R743’s stage-4 section carries the pre-synthesis registry cut and its verify-don’t-assume obligation, the deletion of capture’s extension merge and first-wins claim, the two open decisions (introspection types in or out of the census; applied-directive ordinal stability across base-then-extension merge, pinned by a multi-extension fixture), and the availability cliff resolved as per-census currency rows. Re-open trigger: R743 discarded or re-scoped away from stage 4.

  • Discarded: Delivery verdict derives from the store, not from a hand-maintained negative-space switch (delivery-verdict-derives-from-the-store, R666): dropped at the user’s direction when R682 (planners-read-facts-emitters-read-commands) was repointed to own the leaf-zoo dissolution outright. The item had grown into the roadmap’s last strangler-shaped migration: it derived one verdict as three views plus a shadow test and a residue record while deliberately flipping no production read, so the walk stayed the only oracle and six Spec passes accumulated their corrections in the description of the walk’s holes rather than in the rule set. Its own scope section already recorded the alternative as an open question raised from outside: fold the delivery verdict into the planner item on the same footing as the other three homeless folds (operation members, connection synthesis, tenant bindings), so the consuming slice’s own test is the specification and neither DeliveryResidue nor DeliveryShadowTest is built. That alternative is what the user chose, and R740 (retire-oracle-diff-shadow-tests) independently argues the general case against oracle-anchored derivations. The design analysis survives in git history for the slice that builds the delivery relation: the seven-arm table (four rule arms, three exemption arms), the four predicate warnings (the binding is intent_resolved_type_binding rather than intent_bound_table, record-handedness is the backing closure rather than the producer-payload seed set, authored type and cardinality read through graphitron_field_synthesis, the structurally declared connection’s element resolves through no shipped relation), and the sibling relationship to intent_field_separate_fetch.

  • Discarded: Consolidate graphitron-lsp navigation, dispatch, and result-building (lsp-structural-consolidation, R347): the umbrella’s delivered half stands, its remaining half was overtaken. Slices 1-3 shipped under this item and survive as the module’s shared primitives: one navigation home (Nodes grown into the toolkit plus GraphqlNodeKind as DeclarationKind’s structural sibling, retiring 12 `childOfKind copies and three latent NPEs), DirectivePolicy centralizing the directive-name carve-outs with Definitions joining the coordinate-driven dispatch shape, and the CompletionProvider/Completions sealed-switch dispatcher replacing the hand-ordered waterfall, with the InferredDirectiveRenderer registry and InlayHintRendererCoverageTest closing the silently-dropped-entry gap. The unshipped slices' premises were then dissolved by the fact-store migration (R595, R589, R610): the module roughly doubled and re-formed around facts/ store readers and the LspSurface/TriggerDispatch matrix, Workspace’s four torn-readable volatiles became the single `StoreAccess handle the item’s own 2026-08-06 fact-base note predicted, the LspVocabulary.load() overloads lost their last caller, WorkspaceFile gained close discipline, and the native-library probing duplication left with the graphitron-tree-sitter-natives bundle. A 2026-08-19 audit of the remainder against the tree found three small surviving facts (outbound byte-range to Range construction still per-feature, CodeActions running each rewrite three times per match, the recalc queue’s linear-scan dedup), filed as R739 rather than carried by a 290-line umbrella whose problem table no longer describes the module.

  • R715 (28c4f64 the qualifier split, the folds and the eleven write conversions, 71b7473 the narrowed fold rule, c1dc70c the fact-model page, 0d4e2d1 the Done-gate rework; filed 7594dcd, Backlog → Spec d4f2666, six spec revisions 034a212 + ff2f8a3 + ed6fa19 + 8cb74b9 + 3eb2b78 + 599ce61 + 3474cb2, Spec → Ready 7d4404b, Ready → In Progress df845d2, In Review 4e7c5b9, In Review → Ready 94f6b31, In Review f5ebd84): a decoded grammar lands in a normalized shape rather than being re-flattened into a string every reader takes apart again. Three grammars were audited and two were re-flattened. The dot grammar on nine *_ref columns is now partitioned by capture into a _namespace_part / _name_part pair beside the verbatim value, unconditionally and with no fallback arm: NULL means no period appeared, the empty string means one did with that side empty, and a half-empty reference joins nothing, which is the stated outcome rather than a rule a reader has to know. QualifiedNameGrammar lives in graphitron-model beside the columns it fills, because the split has two writers in two modules and the seeding harness is upstream of the generator. The field-set grammar’s nesting stops being rendered: graphitron_federation_key_field.field_path becomes the child relation graphitron_federation_key_field_segment, FieldSetGrammar.paths returns segments rather than dotted strings, and its tolerance contract is unchanged. graphitron_argument_path_segment is re-keyed from the interning (graph_name, argument_path, position) onto the coordinate (graph_name, type_name, field_name, argument_path, position) with the foreign key to graphql_field that coordinate makes available, reversing the table comment’s own repeating-group defence (a 1NF word carrying a 3NF worry, over a duplication with no update anomaly to prevent and no entity to intern against); intent_type_backing_seed’s join is widened to the new coordinate rather than left correct by absorption. Case folds become generated columns (GENERATED ALWAYS AS (UPPER(...))) on both sides of every crossing, chosen over a written column with a `CHECK because CHECK (x_upper = UPPER(x)) admits UNKNOWN and measurably accepts the drifted row. The fold rule narrowed during implementation and the narrowing is the item’s most reusable output: a fold is minted only where an authored spelling meets a catalog name, a comparison inside one family mints nothing and reaches an existing fold by joining that relation on its key, and a derived view never forwards a fold. That took sql_constraint_column out of the set, so eleven relations converted and not twelve. Because h2 rejects an insert that so much as names a generated column and FactSink.flush() named every column a relation declares, those eleven gained written statements in FactWrites (thirteen newRecord sites, conflict behaviour stated per relation instead of read off a jvm_ / sql_ name prefix), with flush() dispatching to a write function where one exists and rendering generically where none does, because the two arms interleave across the foreign keys and the parents-first ordering has to span both. That is a declared prefix of roadmap/capture-declares-the-columns-it-writes.md’s 123 relations, not a second mechanism, and that sibling’s body now says eleven, thirteen and 112. The views stop taking strings apart: `intent_spelled_table, intent_field_reference_step_hop, intent_column_match_claim and intent_name_matched_key_pair compare stored columns, taking the schema’s reducible budget of 30 UPPER, 8 POSITION and 5 SUBSTRING to zero, 0 and 0, the residual 2 POSITION and 4 SUBSTRING being the bean-accessor rule and two recursion cycle guards that are not case folds at all. WrittenStatementCoverageTest gates the write path without a roster of any kind: every relation the catalog reports a computed column for has a write function, and every write function round-trips a row whose every writable column carries a distinct value, so a dropped column comes back null and a swapped bind comes back swapped. The proposed no-UPPER-in-a-view gate did not land, and the item says why rather than rounding it off: the count reached zero and a concurrently-landing node-identity defect view brought four back, whose two operands are both crawler-produced and so cross no namespace, filed as R724 rather than guessed at. Independent-session In Review → Done review, second cycle: full reactor green under mvn install -Plocal-db (14 modules, 0 failures), no code-string assertions on generated method bodies, user-facing-doc check clean over the one .adoc file, no Retired vocabulary section to sweep though the retired field_path was confirmed gone from every prose surface. All four findings of the first gate were re-verified against the tree rather than taken from the resolution note that the same session wrote: eleven folded relations counted off the DDL, thirteen newRecord sites counted off the capture layer, the sibling’s corrected accounting read in place, and the dropped timing duty confirmed as a structural claim every one of the eleven writers satisfies. The two findings that gate recorded as clean were re-established independently: all eleven written statements agree on column list, marker arity and bind order (checked mechanically, 11/11), and the three rewritten predicates are faithful, POSITION('.') > 0 to namespace_part IS NOT NULL and UPPER(COALESCE(a, b)) to COALESCE(a_upper, b_upper) on UPPER(NULL) being NULL. One finding, not held against the gate because it postdates the delivery: intent_resolved_node_key_projection landed on trunk 91 minutes after this item entered In Review and brought two further per-row UPPER, so the schema holds six and not the four R724 and fact-model.adoc both describe as the single survivor. Unlike R724’s hedge that comparison matches an authored argMapping segment against a catalog column name, so it is a crossing this item’s fold rule does reach; R724’s scope sentence is corrected here, and the premise in R668 that still justifies the per-row fold by appeal to intent_spelled_table and intent_column_match_claim comparing under UPPER() is left for that item’s own gate, which it is still In Progress ahead of.

  • R693 (passes 1-2 landed inside the squashed clone baseline c07c57b, individual SHAs unavailable post-squash; third pass a50b442; third In Review → Ready gate e0736f8, the first two gates likewise in-squash): Flatten a nested grouping input onto a consumer bean at @service, the member-axis sibling of R336. An SDL input type on a bean-typed @service param may now cluster fields under nested input objects while the backing Java class stays flat. The binding rule is implicit, directive-gated, and three-way: a nested input field whose binding key names a member binds as today (a nested InputBean leaf; adding a member is how an author opts a group back out); a directiveless nested field naming no member is a grouping, whose own fields hoist onto the enclosing bean’s binding-key index under the access path ["g", <leaf>] and bind by the normal rules, recursively; a nested field carrying @field(name:) or @nodeId whose named member does not exist rejects, naming the directive and the missing member, so a typo never becomes silently-different behaviour. @table on a grouping input stays inert (the type flattens exactly as its directiveless twin). D1: CallSiteExtraction.FieldBinding and ValueShape.FieldBinding replace sdlFieldName with an ordered non-empty List<String> accessPath plus a mapKey() accessor returning the last element (the wire Map key; leaf() was taken by the extraction component); replaced rather than added, so every consumer moved under a compile error, and a top-level binding is a one-element path with byte-identical emit. D2: InputBeanResolver.buildInputBeanBody fills sdlByBindingKey by a recursive descent applying the rule; the record arm’s bijection keeps its shape (direction A now finds hoisted components, direction B checks index entries so a hoisted leaf naming no component still fails), the JavaBean arm stays partial for scalars. D3 rejections, each a structural Rejection naming the dotted path: cyclic grouping (SDL-type-name expanding set, since a flattened group has no Java class for the existing visited-set guard), list-shaped grouping (list-lifting the leaves considered and declined: it would silently transpose the client’s wire shape), collision as one rule (a hoisted leaf is a peer of the enclosing type’s own fields; checked once against the fully-built index, the path is message-and-emit data and never identity), no depth limit (the cycle guard bounds descent). D4: the emitted helper opens one Map<?, ?> descent local per distinct group prefix, parent before child, via an instanceof pattern with an empty-map default, so no per-field arm needs a null guard and an absent, null, or non-Map group yields null for every member hoisted out of it; the member axis has no omitted-versus-null tri-state, so this matches omitting the leaves at top level. D5: "Grouping input fields under a nested input type" section in docs/manual/how-to/handle-services.adoc, distinguished from the argMapping dot-path (which rebinds method parameters, a different axis). Five new loud JavaBean-arm build failures, deliberately: today bindJavaBean skips an unmatched SDL field before ever descending, so every one of these builds while silently dropping the group’s data on the floor. After this item, each fails the build naming the SDL that caused it: (1) a group whose hoisted leaf key collides with a top-level field’s key; (2) a directiveless group that reaches itself; (3) a list-shaped group; (4) two groups hoisting the same key; (5) a group carrying @field(name:) or @nodeId whose named member does not exist. None can fire on a schema whose data currently arrives intact; a consumer hitting one was already losing that data, and the break is the item working. Deliberate scalar-axis asymmetry: an unmatched scalar field is still skipped in silence; the arm’s partial-by-design tolerance narrows exactly as far as the item can see (a nested field must be inspected to flatten, a scalar drop needs R695’s diagnostic machinery). The record arm is reject-to-accept only: every newly admitted schema failed its bijection before, so no record-arm schema that builds today stops building. In-scope fix of a pre-existing emit defect: the singular nested-bean helper’s Map<String, Object> parameter forced an unchecked cast at its call site, a hard failure in a consumer build under -Werror; widened to Map<?, ?> with instanceof-pattern narrowing at every call site, the plural helper’s per-element cast wildcarded and its @SuppressWarnings dropped. That rule is stated on InputBeanInstantiationEmitter.buildSingularHelper / buildPluralHelper and enforced by graphitron-sakila-example’s `-Xlint:all -Werror compile of the emitted tree, whose FilmReviewGrouped fixture keeps a singular nested-bean member (headline) inside the gate’s reach; a redundant text lint was considered at review and declined, since it cannot tell a covered cast from an uncovered one without scope analysis. The list-valued twin ((List<String>) root.get(k)) is R703, where the remedy does not transfer. Coverage: pipeline InputBeanGroupingPipelineTest (16 cases, both arms: accept cases asserted structurally on the resolved ValueShape.FieldBinding access paths and leaf shapes, rejections by message substring, no generated-body string assertions), compilation tier via the depth-2 FilmReviewGrouped fixture against a real consumer record at <release>17</release> (parent-before-child descent order is a compile error there), execution tier via submitGroupedReview_flattensTheGroupOntoTheBean round-tripping a present group, an absent group per level, and a matching-member group still arriving nested. Retired: sdlFieldName on both FieldBinding encodings (survives only on out-of-scope carriers: SetColumn, KeyColumn, GraphitronType, PayloadConstructionShape, InputRecordShape, PayloadSdlField, the walker Contribution records); the sweep also caught a bindField local that held a dotted path under the retired name, renamed fieldPath. Three independent In Review gates: the first relocated five banned body-string assertions to their real enforcers, the second accepted that relocation but caught one javadoc clause routing readers to a lint that was never written, the third confirmed the clause now names the -Werror enforcer and approved (full reactor green under mvn install -Plocal-db, 14/14 modules, InputBeanGroupingPipelineTest 16/16, user-facing-doc check and retirement sweep both run). Deferrals filed: R694 (bean-helper dedup keyed by class hides divergent bindings, the R437 twin), R695 (scalar-axis unbound-field diagnostic, now the remainder of this item’s contract-narrowing), R703 (list-valued cast twin), R707 (the sibling `JooqRecordServiceParamPipelineTest’s own body-string assertions).

  • R710 (95df965 the delivery; Backlog 968592d, Spec cb11feb, spec revision d4f78a6, Spec → Ready c07c57b, Ready → In Progress d7dc3e0, In Review b910259): the jOOQ crawler records the node-identity constants a generated table class publishes as stated, not as validated. Before this the store held no record of __NODE_TYPE_ID / __NODE_KEY_COLUMNS at all; the metadata existed only as a live reflection result behind JooqCatalog.nodeIdMetadata, which answers a validated question, so a malformed constant was indistinguishable in the store from a table publishing nothing. Two base relations in the sql_ family now transcribe them: sql_node_metadata, keyed on sql_table’s own key with a closed form arm per constant (STRING/NULL/OTHER/ABSENT` and FIELD_ARRAY/NULL/OTHER/ABSENT) plus the stated value and the deterministic class-FQN witness, and its ordered sql_node_key_column entries carrying the array index the encoded identity depends on. A row exists when the class declares either constant, so the half-declared pair the live probe folds into silence is visible, and CHECK (NOT (type_id_form = 'ABSENT' AND key_columns_form = 'ABSENT')) keeps "no row" meaning "publishes nothing". Well-formedness moves out of capture into intent_node_metadata_defect, one row per defect over a closed ten-arm vocabulary with no first-failing short-circuit and no reason-text column, every join inside the jOOQ corpus so it stays a derivation over one corpus rather than a validation smuggled into a crawler; KEY_COLUMN_UNRESOLVED mirrors findColumn’s two-tier case-insensitive spelling against `sql_column.jooq_name or column_name rather than the SQL tier alone. JooqCatalog gains NodeMetadataFacts and nodeMetadataFactsOf beside its four existing capture-facing siblings, with StatedConstant and one shared getField probe so the reading half and the reduction cannot disagree about whether a class declared anything; reflection and raw-Object interpretation stay at that one containment site. The sql_ family charter is amended in the same change, having said "database" while this is the first whole relation of generated-model facts under it, where sql_table.class_fqn had put only a column. No behaviour change: the validating path keeps its exact answers, nothing reads the new rows yet, and generator output is unchanged; the reader arrives with the sibling nodehood item. Held by the two form-to-nullability correspondences and the both-absent exclusion as CHECK constraints, entry parentage and position density as FactSchemaGateTest cases, both relations in FactCaptureAgreementTest’s EQUALITY arm with the view as DERIVED, and an agreement assertion comparing the store’s per-table verdict against the live probe’s, called with the qualified spelling so a shared bare name cannot let the gate agree by luck. Independent-session In Review → Done review: full reactor green under `mvn install -Plocal-db (14/14 modules, 5679 tests, 0 failures), no code-string assertions on generated method bodies anywhere in the delivered tests, all ten defect arms covered by seeded rows in the module whose DDL declares the view, reviewer rule confirmed by session trailer on the implementation commit, and both the user-facing-doc check and the retirement sweep skipped (no docs/ changes, no declared retired vocabulary). Two sub-threshold notes recorded rather than filed: NodeMetadataFacts is the one capture-facing sibling without a compact constructor, so its null-tolerant keyColumnNames is unmodifiable only when reduceNodeMetadata builds it, where IndexFacts copies unconditionally; and the agreement assertion’s MALFORMED arm is vacuous over the fixture corpus, which produces no malformed generated class, as the spec anticipated when it pinned the defect derivation against stated rows instead.

  • R323 (4085a53 the delivery, 3b33f2e the follow-up item’s id; Spec 49b734b, two spec reviews a99e24c`+ed1a601, Spec → Ready `86229aa, Ready → In Progress 7467601, In Review 53901b9): multi-parent NestingField sharing admits the two alias-projected leaves, and the remaining BatchKey deferral stops being an open question. A plain-object nesting type declared on two @table parents has each parent classify the shared type’s leaves against its own table (the anchor), and GraphitronSchemaValidator.compareNestedFieldsShape checks the results agree; arms existed for ColumnBackedField, TableField (R23) and NestingField, and everything else fell to a catch-all deferral. Re-derived at Spec time against the post-R431/R432 model, that catch-all was the sole blocker for exactly three leaf classes, because a leaf reaches the multi-parent question only if isNestedWireableLeaf admits it at nested depth at all: ComputedField and ColumnBackedReferenceField (both admitted at nested depth by R645) and the SourceShape.Table arm of BatchedTableField. Every other BatchKeyField carrier is rejected per-variant first by validateVariantIsSupportedAtNestedDepth (the Record-sourced BatchedTableField arm, ServiceTableField, ServiceRecordField, BatchedPivotField, BatchedInterfaceField / BatchedUnionField, and BatchedTableInterfaceField), so it never reaches this gate on its own account. The first two admit on R23’s own argument and needed no emitter work: ProjectionCommands mints the projection unit per anchor and its $project receives the anchor’s own table local, while the shared <NestedType>Fetchers registration reads the value back by __rk_<resultKey> alias off the source record without consulting the parent, so first-parent-wins registration of the shared nested type is output-identical whichever parent registers it. Stating the rule the arms are instances of is half the item, and it now sits on validateNestingParentCompat’s javadoc in place of a sentence stale since R23: a leaf is multi-parent-safe when every unit and method address it mints under a nested type carries the anchor, and an arm compares exactly the inputs of the addresses that do not. The new arm is keyed on the `ResultKeyAliasedField capability rather than on two class identities, and its two non-obvious constraints are both reified rather than remembered. It sits below the TableField arm because TableField is a member too, so an arm placed higher would swallow it and silently stop comparing filters() where one generated condition method serves every reuse site; that message had no test, and the new negative test makes the ordering a build failure instead of a review note. PivotField is excluded in the predicate rather than merely absent: it is a legitimate member (it does project under a result-key alias) but its unit is addressed pivotUnit(parentTypeName, fieldName) and a nested leaf’s parentTypeName is the nested type, so its address does not carry the anchor. isNestedWireableLeaf keeps it off this gate today, making the exclusion unreachable, but a membership-only predicate would pre-answer the multi-parent question in the affirmative for any future member with nothing to force the check, which is exactly the hand-declared-marker drift development-principles.adoc names. The arm compares domainReturnType() as the read-side analogue of the ColumnBackedField arm’s columnClass check, plus, on the reference half, the joinPath() terminus read straight off the model through JoinStep.HasTargetTable (this validator holds no catalog, and ServiceCatalog.terminalTableForReference answers empty for a non-FK-derived terminus where the comparison still wants both sides). That second fact is not belt-and-braces: a {key: "..."} first step resolves from either endpoint of the named FK, so two anchors sitting on opposite ends traverse it in opposite directions and read two different tables' same-named, same-typed column, which domainReturnType() cannot see; terminal table rather than terminal column name is the grain, because that divergence shares the column name. One draft claim was corrected on contact with the model and recorded rather than quietly fixed: ComputedField.domainReturnType() is not the peeled helper return type, because OutputField.peelToClassName does not unwrap org.jooq.Field, so every instance answers the constant Plain(org.jooq.Field); the base comparison stays as the uniform check but is discriminating on the reference half only, and the honest reason it never fires on the computed half is that every fact that leaf stores bar its per-variant-deferred joinPath derives from the single SDL declaration on the shared nested type. Also here: the class-mismatch check routes a ResultKeyAliasedField membership split through the class-carrying Rejection.deferred overload instead of structural, worded on the FK-orientation fact that actually differs (the only route there is an @nodeId FK-mirror collapse landing on different sides per anchor, and the reference side always co-rejects on its own NodeIdEncodeKeys deferral, so the wording deliberately avoids naming a projected-alias read that side never performs); the catch-all moves onto the same overload and its comment now states the rule plus today’s sole resident, the Table-sourced BatchedTableField arm on both authoring shapes (a plain @splitQuery and a lookup-keyed instance, since isNestedWireableLeaf gates on sourceShape() and reads nothing about lookup()). That deferral stays and becomes the documented end state rather than an open question: GeneratedUnits.rowsMethod addresses the rows method on the nested type so every sharing parent mints the identical reference and the coordinate gets one DataFetcher, while deriveSplitQuerySource reads the batch grain off each anchor’s own correlation columns, so admitting it needs per-anchor minting plus a runtime dispatch inside the single registered fetcher on which anchor the arriving source row came from, and where that discriminator lives on a projected source row is a genuine design problem with no measured downstream demand. No successor item is filed by design; a future admission should be filed fresh, with demand in hand, against whatever the minting machinery looks like then. ResultKeyAliasedField’s javadoc said "the four families" and named three; it now names `PivotField too. Coverage: NestingFieldValidationTest gains both admissions, both divergence rejections (domainReturnType() and terminal table, the latter hand-built on the reverse-{key:} shape and the one case that fails without the second comparison), the membership-split rearm with the test’s own comment recording that no reachable schema produces that message alone, and the arm-ordering guard; the catch-all’s deferral witness migrated off ColumnBackedReferenceField (now admitted) onto a Table-sourced BatchedTableField so the message keeps a live pin. GraphitronSchemaBuilderTest pins per-anchor classification of a shared type carrying both leaves (two different FKs, one terminal table) and the cross-parent R646 assignability rejection, asserted at the outer coordinate because the nesting arm of FieldBuilder.classifyChildFieldOnTableType rewraps a nested UnclassifiedField there, with the consequence asserted deliberately: the rejecting parent’s field is no longer a NestingField, so the shared group drops to one member and compareNestedFieldsShape never runs on that schema. Execution extends the R23 OccupantLocation fixture (shared by Customer and Store, both FK to address) with one leaf per admitted class, a {table: "address"}-entry scalar @field + @reference and an @externalField whose helper takes Table<?> (the form a shared nesting type requires, since the helper is reflected once per anchor), both asserted against their flat siblings under both parents. No @classified corpus verdict flipped, as expected for a validation-time gate. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db (14/14 modules), no code-string assertions on generated method bodies, reviewer rule confirmed by session trailer on both implementation commits, retirement sweep clean across all three declared terms (surviving hits are the gitignored docs/target/staging render and the historical R23 changelog entry), and no docs/ changes so the user-facing-doc check is skipped. One sub-threshold note recorded rather than filed: the rewritten catch-all comment says other BatchKey carriers "never reach here", where they do reach it and collect a second, redundant error on top of the per-variant rejection, as the spec body itself stated more precisely. Found on the way and filed as R708: the generated $project selection gate switches on graphql-java’s getFieldsGroupedByResultKey(), which is recursive over the whole sub-selection, so a unit’s arm fires for any name selected at any depth below it; the fixture leaves are named occupantDistrict / occupantAddressId to keep this item’s shape off that defect rather than re-pinning ProjectionSqlBaselineTest onto a spuriously projected column.

  • R704 (e1d9df5 Track A slices 1-3/5-6, c04ffc2 pagination, 482e8a8 routine catalog facts, 306972c chain terminus + name-matched hop arm, 59a746b`+fb851b1` the return binding, a302874 the keying rule as a relation, 68c639f self-review; Ready 36a346a, In Review 1d9d71c): The @routine read surface, unwired and then re-derived from facts. A table-valued function’s result table has no primary key and no foreign keys, and the generator restated that one catalog property as five unrelated refusals in five files: a root @routine field silently dropped @defaultOrder, deferred @orderBy and @condition, rejected @asConnection, demanded a restated @table on its return type, and refused a child’s implicit hop out of the result. Track A deleted the carve-outs rather than building capabilities, the render layer already being generic over all of them: both chain classifiers now resolve the whole read surface through resolveTableFieldComponents against the chain terminus, with the routine’s own IN-parameter arguments excluded off the resolved bindings rather than off the directive text; validateListRequiresOrdering lost its Chain exemption and grew a routine arm, the generic message’s "add a primary key to the target table" being impossible on a function result; pagination needed the arm built rather than unblocked, and composing the hops into a joined table expression is what makes one Table<?> local the FROM, the seek’s source and the connection carrier’s count source at once, so totalCount counts the chain rather than the terminus alone; parsePath’s element-less arm gained the function-ness gate its `{table:} sibling already had. Track B replaced the hardcodes with relations: sql_table.table_type, sql_routine and sql_routine_parameter capture the catalog facts (jOOQ generates no Routine object for a table-valued function at all, which is why the callable is its own subject and table-valuedness is the join); intent_field_chain_terminus states where a chain lands and on what kind of table; intent_routine_return_binding and intent_resolved_type_binding make a routine’s result bind its return type at type scope, with disagreement stated as candidates rather than picked; intent_name_matched_key_pair states the keying rule catalog-only, and the hop view’s new NAME_MATCH arm is its first reader. The classifier half sits at the producing edge beside the carrier’s own fixed point (RecordBindingResolver.groundRoutineReturnType plus TypeBuilder.routineReturnVerdict), because minting the binding globally would collapse the fork classifyMutationRoutineCarrier decides by asking whether the return is already table-bound; three readers of the written directive moved with it. The sakila fixtures keep one spelling of each, ActorFilm derived and Tilgang written, so the execution tier proves them equivalent against a real database. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db (14/14 modules), no code-string assertions on generated method bodies, and the deterministic-order enforcement mutation-checked at the gate rather than taken on report (restoring the Chain exemption fails ValidateListRequiresOrderingPipelineTest.routineTerminusListWithNoOrderingNamesTheFunctionAndPointsAtFields). Four slices left the item deliberately and are filed: the carrier’s explicit data-field path as R717, the three reads that did not catch up as R718, the carrier-versus-chained discriminator as R719, and the plan-tier pilot onto R682’s launcher step, which is recorded on that item at this gate rather than left in a deleted file. Three residues filed as R720 at the gate: prose in QueryField and RoutineResolution still naming @table where the code holds the binding, bindReturn’s rejection forking on whether `@reference was written rather than on where the chain ends, and one execution-tier order assertion whose fixture row order already equals the order it asserts. The two Java copies of the keying rule (synthesizeNameMatchedJoin, deriveRoutineCarrierPairs) deliberately still run: the re-plan established that they retire when their consumer becomes a planner reading facts, not by teaching the classification walk to read a view, so the two new relations have no generator consumer yet and the store’s own tests are what read them.

  • Discarded: Routine carrier: admit the explicit data-field reference path, single- and multi-hop (routine-carrier-explicit-data-field-path, R622): superseded by R704 (routine-composition-surface-from-facts) at the user’s direction, from Backlog, before any implementation. R622 owned the write-side half of one gap R704 owns the read-side half of: a hop from a routine result to a @table-bound target. The two seats had exactly inverted affordances and the same derivation written twice. On the read side an author must write @reference(path: [{table: "rolle"}]) even though the child’s return type already names rolle, and the implicit form is rejected with an FK message that steers away from the single-hop spelling that would work; on the carrier’s data field the implicit name-matched hop is the only spelling and @reference is deferred outright. BuildContext.synthesizeNameMatchedJoin and BuildContext.deriveRoutineCarrierPairs are the same loop over the target’s primary-key columns against the routine result’s column names, differing in rejection text and return type. Closing one cell without the other would have left the inversion standing with the duplication still funding it. R622’s own framing anticipated the fold: it declined to spec a new parse seat on the legacy pipeline and asked to be specced as views over graphitron_field_reference_step, naming itself a candidate first consumer pulling the carrier-classification neighbourhood onto the store. That reading is confirmed and understates the position. intent_field_reference_step_hop already enumerates every hop a path element can express and intent_field_reference_step_target already walks them recursively from the enclosing type’s binding, so multi-hop path resolution, which R622 called its real remaining work, is a shipped derivation. What is missing is one arm: both hop-view arms join sql_referential_constraint, so a hop departing a function result yields no rows. A name-matched arm gated on the function-ness discriminator R704 captures serves three consumers that run three separate code paths today, two of them already copies of each other. Nothing is lost: R704 carries R622’s residual-path design work (ParentCorrelation.checkCarrierInvariant pairs a non-empty joinPath only with a hop-anchored correlation, while the carrier’s is the hop-less OnLiftedSlots, so a residual path needs a correlation arm anchoring on the captured record) and its inherited-as-decided constraints from the shipped carrier item (the two-statements rule at every hop count, post-commit hops running under the caller’s identity, and a multi-hop data field resolving null with empty errors being a documented success outcome rather than a defect). Re-open trigger: none; if R704 is descoped to the read side, the carrier path becomes a new item.

  • Discarded: @defaultOrder on a root routine chain is silently dropped (routine-chain-order-directive-silent-noop, R659): superseded wholesale by R704 (routine-composition-surface-from-facts) at the user’s direction, from Spec, before any implementation. R659 reported one axis of the @routine read surface and diagnosed the shape behind it precisely: four independent read-surface axes governed by one boolean predicate in RoutineDirectiveResolver.orderOrConditionDeferral, one four-way conjunction in QueryField.QueryTableField’s compact constructor, and one `List.of() literal per chain classifier, with nothing binding the three. It then declined to fix the class, recording the shared enforcement question as "nobody’s rider". Two findings made that split untenable. First, the census widened: the same hardcode also refuses @condition, @orderBy and @asConnection, and refuses the return type any spelling but a restated @table. Second, and decisive, R659’s premise that @orderBy and @condition are honest capability deferrals is false. The render layer is already generic over both: OrderingBlock.declareSortView is total over Ordering.Columns and Ordering.Helper, RootLauncherRenderer.conditionStatement already treats an absent WHERE as data, routineBody already composes a .where(...) from hop filters, and OperationMember.Condition.OnReturnTable carries a plain TableRef with no key requirement anywhere in the path. Nothing consults a primary key or a foreign key to build a WHERE or an ORDER BY, so all four axes are unwired slots rather than unbuilt capabilities, and they touch the same six sites. Shipping R659 alone would have written the constructor-pin restatement, the deferral message and the manual’s deferral sentence twice each, with the intermediate version published false. Nothing is lost: R704 carries R659’s field report, its breaking-change population analysis (every single-node root routine list in the wild, since a TVF result table has no primary key), the two author-facing messages that currently tell a routine author to add a primary key to a function result, the measured ordering-target study on PostgreSQL 16 (naming the routine column and the joined catalog column produce byte-identical plans, so ordering target is not a performance lever), the falsified-prose inventory, and the per-tier test plan. Inbound citations repointed in the same commit (R448, R660, R662’s depends-on, R663, R677). Re-open trigger: none; if R704 is descoped back to the architecture half, the read-surface unwiring becomes a new item rather than a resumption of this one.

  • R57 (6a7b22d the delivery, efd37f3 the second-pass rework; Spec → Ready on the fourth independent Spec review 804af44, In Review → Ready 813ce39, Ready → In Progress d453e67, In Review bab8626): an @nodeId(typeName: X) argument or filter input field whose containing table reaches X.table() through a foreign key targeting columns other than X’s key columns used to be rejected at classify time, deferred to output-side JOIN-with-projection emission. That dependency was false in direction: projection is the encode side (turn a parent key into a result column), this is the decode side (turn already-decoded keys into a predicate), and nothing about the decode side is unsupportable. The decoded key simply has no column on the field’s own table to compare against, so SQL visits the target table and compares its key there, which is a correlated `EXISTS over the FK, the exact BodyParam.RemoteColumnPredicate shape a joined plain @reference filter already emitted. EXISTS is also the semantically right shape rather than a convenient one: no row multiplication when the path is non-unique, and a NULL FK column fails the correlation instead of duplicating or dropping rows. The read path now emits it at both surfaces; the write and @lookupKey rails keep a deferral stated in their own words. The structural half is naming an axis that had three implicit spellings, all answering "which table does this carrier’s value predicate bind against": an empty-joinPath sentinel on ArgumentRef.ScalarArg.ColumnBackedArg, a Direct-vs-NodeIdDecodeKeys extraction test in FieldBuilder.remoteIfReferenceJoin, and a column slot whose referent depended on which case produced it. The extraction test encoded "`@nodeId` implies local", which the translated shape falsifies outright. All three collapse onto the sealed FilterBinding with arms Local(List<ColumnRef> ownTableColumns) and a payload-free Remote; it replaces liftedSourceColumns on both reference carriers and joins ColumnBackedArg beside its joinPath, so wrapIfRemote is the one discrimination and remoteIfReferenceJoin / translatedFkRejection are gone. Remote is payload-free because in both cases reaching it the terminal tuple already is the carrier’s columns(), so only Local carries a tuple columns() cannot supply; joinPath stays deliberately orthogonal, since a Local-bound FK-target with an authored @condition still needs the path for the FkTargetConditionFilter correlation. The invariants live on the carriers, not on the payload-free arm, which is the only place that can see the path: Remote requires a non-empty joinPath, and on ColumnBackedArg (where the local tuple restates columns()) the arities must agree. Retiring the accessor made javac name every reader, with one exception found by hand and worth recording as the general rule: MutationInputResolver.admitMutationInputFields dispatches on the carrier without reading any binding-sensitive slot, so it would have silently admitted a Remote carrier into INSERT until an emitter tripped over it. Four rails now gate with an exhaustive switch so a third arm breaks them all at compile time (INSERT, UpdateRowsWalker.classifyInto, DeleteRowsWalker.classifyInto, FieldBuilder.classifyPlainLookupKeyArg), and they share the message text minted by FilterBinding.remoteBindingUnsupported rather than one Rejection instance, because the walkers' channel is a Rejection.AuthorError sibling a Deferred cannot inhabit; write-side emitters read their columns through one Local-destructuring accessor that throws on Remote, so a bypassed gate fails loudly instead of emitting a wrong statement. Execution fixtures landed in the public schema as the spec decided, costing CREATE TABLE`s plus seed rows in `init.sql, two NodeIdFixtureGenerator.METADATA entries and SDL in the example module’s existing schema.graphqls, with no new Maven execution, jooqPackage or .graphqls: xlat_parent / xlat_child and the composite xlat_comp_parent / xlat_comp_child whose child FK targets a different composite unique constraint. Coverage is TranslatedFkTargetRailGatesPipelineTest (all four gates plus a direct-FK carrier that must still be admitted, asserting the shared text rather than four substrings), the two rewritten NodeIdPipelineTest cases now asserting the carrier shape and the one-hop predicate, the two carrier-invariant tests, and TranslatedFkTargetFilterExecutionTest (list and scalar argument forms, the input-field form, empty and omitted lists contributing no conjunct, a childless parent, the composite twin, malformed and wrong-type ids throwing). Independent-session In Review → Done review, two passes. The first pass passed the delivery against the contract and blocked on one introduced defect: the plain-@reference argument arm passed Remote unconditionally on a precondition it had not checked, and since @reference(path: []) is legal SDL and an argument site calls parsePath with a null target table, the empty path survived, the column resolved against the field’s own table, and the carrier’s own Remote-needs-a-path invariant threw out of classification: a regression from a working bare local Eq to an untyped crash. The invariant was right and the call site was wrong, so the arm now forks on path emptiness and binds Local, mirroring the input-field sibling in BuildContext.classifyInputField whose divergence was the tell; both surfaces are pinned in ReferenceFilterRemoteColumnPipelineTest beside the direct-FK-stays-local guard, so the matrix reads that the binding follows the path in both directions. Whether an inert element-less @reference should be rejected at all is one decision over both positions rather than a property of that arm, filed as R692. Second pass verified at trunk tip: full reactor green under mvn install -Plocal-db, 14/14 modules; reviewer rule confirmed by session trailer on both implementation commits; no code-string assertions on generated method bodies (the execution tier’s SQL_LOG token cross-checks are the sanctioned structural form the test-tier guide names, beside real row assertions); user-facing-doc check clean, the one docs/ change being the widened code-generation-triggers.adoc row; retirement sweep clean across all eleven declared terms, with the surviving encode-side JOIN-with-projection prose and liftedSourceColumns on DirectFk verified as the whitelisted hits the spec named. One sub-threshold note recorded rather than filed: the @lookupKey-is-meaningless rejection text now sits in two literal copies at FieldBuilder.java:1628 and :1645, the pattern remoteBindingUnsupported was minted to avoid one screen below. Deferred rather than folded in, each filed: write-target translation (the scalar-subquery SET / INSERT emission the four gates defer), multi-hop translated paths and condition-join hops, the encode-direction emitter (nodeidreferencefield-join-projection-form), and the manual page whose single-direct-FK no-JOIN claim this item made visibly wrong (R691).

  • R661 (d048054 implementation; Ready → In Progress c487906, In Progress → In Review a193eeb): the discriminated table interface child batches through a DataLoader, closing the one member of the interface family that never batched. Delivery is leaf identity on cardinality, the multi-table sibling’s rule rather than a marker: list cardinality mints ChildField.BatchedTableInterfaceField (in the TableTargetField seal, implementing BatchKeyField and declining ParentRowDemand because the batched arm of ProjectionCommands.tableTargetContribution precedes the demand arm and both answer the same column list), single cardinality keeps the per-parent TableInterfaceField. The sibling’s table-bound-participant conjunct has no analogue and is stated as structural rather than restated as a guard: TypeBuilder.buildParticipantList’s discriminated arm rejects a non-table implementor, and the invariant is `TableBacked rather than TableBound, since a joined-table discriminated interface populates JoinedTableBound exclusively and porting the sibling’s anyMatch verbatim would have left that shape unbatched; GraphitronType.TableInterfaceType’s javadoc, which claimed the opposite, is corrected at the same name. The batch key is the plain batched child’s, the single FK hop’s source side through `deriveSplitQuerySource, so a composite FK keys and correlates on every slot and the polymorphic pair’s empty-PK rejection has no analogue. @splitQuery becomes redundant rather than honoured or rejected (the re-projection is always its own statement, so the directive’s meaning is satisfied before it is applied, and what an author reaching for it wants is now the default): a new splitquery-redundant-on-discriminated-interface-child lint rule with the computable delete fix, emitted through a warnIfSplitQueryRedundant generalisation that puts both redundancy warners on the gathered marker relation instead of a second directive probe. On the emit side composition rather than duplication: LaunchSource.DiscriminatedCorrelatedChain is a third Correlated arm carrying the DiscriminatedTable payload whole beside the chain topology, which is sound because the two halves are orthogonal (the batch grain never reaches the select list, the discriminator restriction never reaches the topology), and projection() moved off Correlated onto a new Correlated.Projected sub-seal so the member stays total where it is declared; BatchedRowsFragments.discriminatedBody binds the shared prelude, parent-input attach and WHERE fold to DiscriminatedTableFragments.projection plus a newly extracted joinedDetailJoins. No alwaysProject column list is threaded after all, the spec’s expectation: the scatter groups by the parent-input __idx__, never by a projected child column. Both delivery-computing sites gained the arm, the compile-forced leaf crosswalk and DeliveryFactRelation.mint, where the new arm precedes the marker reads so the redundant marker cannot claim the trigger at either cardinality; singleTableBackedVerdict’s `false case stands with its rationale rewritten. Two hazards settled by hand that no gate would have caught: the loader registration routes through TenantDslEmitter.loaderNameDeclaration, because a batch loader resolves one DSLContext from the environment captured at loader creation and a tenant-mixed batch would serve one tenant’s rows to another; and cross-table hop Java locals gained a prefix the alias scheme cannot produce, a latent uncompilable-output bug the batched host exposed whenever a hop target and the discriminated base share a first letter (the projected SQL aliases are untouched). Coverage across every tier the claim is visible at: the cardinality fork and its key derivation at the pipeline tier, the mediaList verdict tuple in the table-interface corpus example, the correlation-key arm, the redundancy warning at both cardinalities, the emitted tenant loader-name expression, the launcher census’s own coordinate, three coordinates in DeliveryFactPinTest (plain list plus both @splitQuery-marked cardinalities, which is what turns the pin into a gate over the marker arms), a whole-statement SQL baseline, and the execution-tier statement count with a cross-table participant field proving the aliases survive the loader scatter. splitQuery.adoc gains the author-facing bullet on the redundant side of the line that page already draws, and supported-schema-shapes.adoc regenerated. Independent-session In Review → Done review, one pass, approved clean: full reactor green under mvn install -Plocal-db, retirement sweep clean across all declared sites, all three of the item’s open questions answered in code and javadoc rather than left implicit. The reviewer’s non-blocking notes went to Backlog rather than holding the gate. Unblocks R650 (root-connection-over-discriminated-interface), whose body was updated in the same commit with the two forks this item settled and the hasConnectionSplitField gate its connection half now has to widen.

  • Discarded: Decompose TypeFetcherGenerator (decompose-typefetchergenerator, R7): superseded by R682 (planners-read-facts-emitters-read-commands) at the user’s direction, before any implementation. The item was a planning question, which axis the 6,000-line file is actually long along, with decomposing along the field taxonomy into per-variant emitter classes as its leading option and a layout javadoc as the fallback. R682’s emitter half dissolves the question rather than answering it: the file does not get decomposed along the leaves, it empties into render as the emitter families migrate onto command relations, and R682’s census puts the fetcher family (this file plus FetcherEmitter) at 120 of the 129 leaf-dispatch sites the generators-side ratchets count, so the emptying covers what the decomposition would have reorganised. R682’s body already declared the supersession and scheduled this discard for its Ready transition; the discard landed alongside its Spec instead, at the user’s direction, so no session picks the item up in the gap. The 2026-08-17 staleness audit’s R7 row (refresh the hypothetical emitter names in the option list) is superseded by the same motion. Nothing else is lost: the layout-javadoc fallback lives on as source-orientation-javadocs.md’s partial-mitigation option, and the R6 `FieldBuilder precedent it cited is already in the changelog. Re-open trigger: none; the file’s shape is R682’s scope, and a future navigation-only complaint against a file R682 has emptied would be a new item, not a resumption of this one.

  • Discarded: The emit plan is built from the store, not from the leaf model (emit-plan-reads-the-store, R667): absorbed into R682 (planners-read-facts-emitters-read-commands) at the user’s direction, the same day it was repointed and before any implementation, on the reasoning that the architecture is easier to do properly in one item. The two were filed apart on the premise that the planner half was blocked on a fact population that did not exist yet while the emitter half was not; measuring the DDL dissolved that premise. The expensive population had landed (the per-coordinate classification stratum, which the language server already reads arm by arm), and what remained was four relation-shaped folds with no home (operation members, connection synthesis, tenant bindings, delivery), none of which needs a new rule. Both halves became sequencing problems rather than modelling problems, and sequencing them against each other is the question neither item could answer alone: converting all six relations before any emitter moves leaves emitters reading leaves for the whole programme, while converting emitters first mints command relations from leaves the planner half then re-sources. Nothing in the item is lost. R682 carries its body substantially whole: the measured thirteen-accessor read surface, the six-relation dependency order with the per-relation-increment argument, the three store populations, all four risks, the retired vocabulary and the coverage obligations, plus its declared dependency on R666 (delivery-verdict-derives-from-the-store), inherited verbatim. Its "out of scope" entry for renderers, which named a renderer reading the model directly as a finding to file rather than to fix, is what produced R682 in the first place and is now in scope there. One thing did not survive, and it predates this absorption: R678 (capture-expands-facet-synthesis) was absorbed into R667 under its former slug (capture-precedes-the-classification-walk) as "deliverable 3", and the repoint onto the emit plan dropped that deliverable without recording it, so that entry’s "Nothing is lost: R667 carries the template" has been false since 456ee94. The question is re-filed as R683 rather than carried into R682, because its motivating reason (a store-reading classifier that cannot mint facet types) lapsed with the same repoint and needs re-deciding rather than re-inheriting. Re-open trigger: none; the plan’s conversion is R682’s scope.

  • R617 (0bf512d root-only scoping, 33218aa rejection attribution and code-string removal; the four earlier implementation SHAs recorded in the item body no longer resolve after a history rewrite): a root lookup returns one slot per input key, holding null where the key matched no row. The manual had documented that positional contract for as long as both had shipped while the generator inner-joined the VALUES table and let a missed key contribute no element, so filmById(film_id: ["1", "999999", "2"]) answered two films rather than three positions; the user settled it in the documented direction, so the emit is what changed. RootLauncherRenderer.lookupBody’s list arm projects the derived table’s `idx as __idx__ and scatters through a new scatterLookupByIdx helper, which is also what carries input order, so the arm emits no ORDER BY, and the launcher’s value type becomes List<Record> because a jOOQ Result cannot hold a null element; two rows landing on one key keeps the first rather than throwing as scatterSingleByIdx does, since a lookup joins on author-declared columns the schema never required to be unique. The contract forces the schema’s hand: [Film!]! cannot carry a miss, GraphQL propagating the null out of the list so one unmatched key would discard every matched row, so GraphitronSchemaValidator.validateRootLookup gains a nullable-element rejection and every existing root lookup declaring non-null elements has to drop the inner !. Root-only throughout: a child @lookupKey coordinate narrows each parent’s list, keeps .orderBy(input.field("idx")), contributes no element for a miss, and correctly keeps [Actor!]!. Coverage: ScatterLookupByIdxTest drives the emitted helper reflectively, including the first-wins tie-break PostgreSQL cannot observe on uniquely-keyed fixtures; the lookup SQL baseline pins the __idx__ projection and the absent ORDER BY; execution pins that languageByKey(language_id: [2, 1, 2, 99, 1]) answers 2, 1, 2, null, 1 and that five repeats of one id answer five times. Four independent Done gates, three of them holding on the manual rather than the emit, which reviewed clean from the first: positional claims stated as measured behaviour, then the NodeId section, then two claims quantified over every @lookupKey shape while describing only the root arm, then the @asConnection sentence attributing one arm’s error message to both. The third gate also removed three code-string body assertions the item had added to RootLauncherRendererTest, each redundant against a sanctioned-tier twin. Spun out rather than folded in: R669 (that file’s remaining pre-existing code-string assertions), R670 (a root @lookupKey plus @asConnection reports a @table error the author cannot act on, and the validator’s connection branch is dead from real SDL), R679 (the child rejection message justifies itself with the root’s positional contract).

  • Discarded: Capture expands @asFacet the way it already expands @asConnection (capture-expands-facet-synthesis, R678): absorbed into R667 (capture-precedes-the-classification-walk) at the user’s direction, within the session that filed it and before any implementation. Filed as a strict dependency of R667 on the reasoning that a classified-model builder holding no GraphQLSchema cannot mint the facet types only a schema rebuild produces, so capture has to expand them first. The reasoning stands and is now deliverable 3 of R667; what did not stand was the partition. The expansion has no independent success criterion and no independent consumer: a facet expansion nobody reads is another shipped-but-unread relation, and the only thing that demonstrates it is correct is R667’s next deliverable classifying a faceted schema with no schema in reach. Separating it would have doubled the workflow gates (two Spec reviews, two Done reviews, each requiring a different session) around a step that cannot be verified alone. Nothing is lost: R667 carries the template (MacroCapture.expandConnections, its carriers-minted-after-the-walk shape and graphitron_type_declaration_synthesis provenance), the twin to pin against (ConnectionPromoter’s facet arm) and its retirement, and the exit criterion (the named facet subtraction leaves `DemandShadowTest’s reach equality). The contrasting case is R667’s other excluded neighbour, the authored-claim gate flip, which stays a separate item precisely because it does have its own criterion, written on `ClaimDomain, and unblocks on a schedule R667 does not control. Re-open trigger: none; the facet expansion is R667’s scope.

  • R639 (5b021e9..935c964 implementation; rework f04b1f2/2320aa4; held at the gate f65cd3f): session identity is a Java method resolved at build time, not generated SQL. <sessionState> stops naming database routines as strings for graphitron to assemble a { call fn(?) } around; <mount>fqcn#method</mount> plus an optional <unmount> name public static methods reflected through ServiceCatalog at build time and called directly, the contract being a signature (exactly one seam parameter typed org.jooq.Configuration or java.sql.Connection anywhere in the list, the rest is payload, the return type is the handle), which jOOQ’s own generated Routines executing methods satisfy as-is, so the headline path is zero hand-written Java and the seam rule doubles as the overload selector against jOOQ’s Field-expression siblings. Deleted rather than carried: the <variables> Postgres sugar and with it the runtime’s non-Postgres dialect guard and the session-state-convention-fence lint rule (a build cannot grade a reflected method’s position on the integrity gradient, so the gradient moves to docs/security.adoc where it is explained instead of asserted); <handle> (the return type states it); <stateSurvivesTransactions> and the whole per-settle re-fire, because acquire asserts autocommit before the mount, so the mount is its own committed transaction and no later settle can revert it, which also takes GraphitronTransactionProvider’s `priorAutoCommit field and second constructor; the generated SessionHook interface, its no-op, the runtime’s hook field and RuntimeHookProjection, replaced by one final class with static mount/unmount and nothing at all emitted under the unconfigured arm. Mount payload parameters are ordinary contextArguments (one name-keyed factory slot each, a fact shared with a @service site entering once, type disagreement caught by the classifier’s existing TypeConflict), reversing an earlier disjoint-namespace decision; CLAIMS_KEY goes with it. Two structural moves carry the rest: acquisition is lazy on every path through one unified per-key carrier (ConcurrentHashMap keyed by Optional<tenantKey>, single-tenant as the one-key case, the whole (connection, dsl, handle) entry minted inside one computeIfAbsent), which is what reduces a pinned connection to exactly one transaction provider (two of them over one connection would commit an enclosing transaction at an inner boundary) and what gives the handle a per-tenant Configuration to ride; and the runtime’s source SPI takes a DataSource/SQLDialect/Settings triple per entry, so consumer jOOQ settings finally reach both the fetchers' SQL and the consumer’s own mount call. The handle reaches service code as the $session argMapping sigil (absorbing the former R640 and dissolving its four forks: ordering, staleness, fan-out scope and opt-in surface), owned by ArgMappingSigil which both the build-side parse and fact capture route through, landing in a sibling graphitron_service_arg_mapping_sigil relation rather than quarantining as undecoded overflow; a $session-binding field under a tenant context classifies Inherited so it reads its own tenant’s handle. Five new ReflectionError permits with their typed-rejection.adoc paragraphs; the provenance family collapses to store_graph_session_mount/store_graph_session_unmount with the foreign key making "unmount without mount" structural. Coverage: SessionHookImplGeneratorTest and TenantConnectionsGeneratorTest compile the real emitted TypeSpec`s and drive them (no generated-body string assertions introduced); execution-tier `SessionHookExecutionTest proves the direct-Routines and facade shapes indistinguishable at the database, RLS scoping across a mutation’s commit and rollback on an autoCommit=false DataSource, wholesale overwrite, fail-closed throwing mount, and the contextArgument unification end to end; TenantFanOutExecutionTest proves two tenants read two distinct handles in one operation. Independent-session In Review → Ready → In Review → Done review; both passes green under mvn install -Plocal-db. The first pass held on one blocker: the item specified a located runtime throw for a $session read on an escape-hatch operation, and what shipped was an unguarded cast binding null into a service parameter meant to carry mounted identity. The rework routes every $session read through a generated TenantConnections.sessionHandle(dsl, fieldCoordinate) accessor that throws naming the coordinate, the sigil and the owned entry points, with the pipeline, compiled, emitter-unit and execution rows the first pass lacked. Three review side-items landed with it: R469’s body repointed off the deleted re-fire, the staged-builder trade-off filed as R664, and the jvm_method_parameter census debt plus the $session completions follow-up named in their LSP items. Absorbs R640; feeds R468.

  • Discarded: Support @asConnection on a child field returning a single-table discriminated interface (child-connection-over-discriminated-interface, R651): absorbed into R650 (root-connection-over-discriminated-interface) at the user’s direction, four hours after filing and before any implementation, when R650’s Spec review reopened over the route question. The two were split on the premise that the root was a contained emission fix while the child’s delivery story was unsettled. The review dissolved that premise: the root plan proposed keeping the participant cross-table @reference join inside the paginating statement behind a new build-time cardinality invariant, and this item’s own decisive argument against its cheap route, that a per-parent paginated child would be strictly more permissive than the plain child on the same authoring surface for no principled reason, applies verbatim to the root. Both coordinates now turn on the same question, whether the paginating statement is a split query, and specifying them apart would answer it twice. Nothing is lost: R650 carries the child leaf’s delivery facts (ChildField.TableInterfaceField, TypeFetcherGenerator.buildTableInterfaceFieldFetcher, N+1 by construction with no DataLoader), both routes with the rejection argument, the unpinned child deferral in its retired vocabulary, and the seam question as open rather than settled, since the projection / joinedStep split was designed for the rejected route. This item’s third open question, that the interface arm never reads forcesSplitDelivery so @splitQuery is silently swallowed, was filed separately as R661 rather than absorbed, because it is a bug whether or not the connection work proceeds; R650 depends on it. Re-open trigger: none; the child coordinate is R650’s scope.

  • Discarded: The rewrite’s architectural history as an explanation page (road-to-the-relational-core, R634): filed and shipped the same day, outside the workflow, at the user’s explicit direction that this was not a regular task. The page exists at docs/history/road-to-the-relational-core.adoc, alongside a new docs/history/ section index; the item’s remaining written content was its own gate. Two things it left open were settled in the writing rather than at Spec. Work is cited by roadmap id inline, with no links to per-item files, because those are deleted on ship while roadmap/changelog.md keeps the ids; and the page defers to the present-tense pages under docs/architecture/ and to the model DDL, stating that where they disagree it loses. The history it records was reconstructed from 354 changelog entries, 603 item files and the 14 dated audits, and its March chapter from 95 pre-squash commits recovered from unreachable objects and published as the graphitron-rewrite-history branch, which is the only copy of that history in existence. Re-open trigger: none as an item; further history pages go under docs/history/ as ordinary docs work.

  • R629 (fe5f35b implementation; Spec → Ready 3499425, Ready → In Progress 368343c): mountable GraphQL-over-HTTP delegates with an explicit operation policy. graphitron-jakarta-rest’s endpoint surface splits out of `GraphqlResource into two public @ApplicationScoped delegates a consumer resource can mount at any path, template parameters included: GraphqlHttpHandler carries the whole decode/decide/execute/encode pipeline with every Jakarta REST input passed as a parameter rather than injected, and GraphiqlBundle carries the self-hosted page and its assets (the {{ASSET_BASE}} rewrite now resolving against whatever path served the page). The GET-mutation guard generalises into OperationPolicy, a final class with three validating factories (queriesOnly(status), queriesOnly(status, message), allowing(Set, status, message)) publishing only permits; the specification’s queries-only-with-405 rule for GET is one more instance of the same type, so there is a single enforcement path and no way for a consumer to weaken conformance. Three design properties carry the boundary structurally rather than by javadoc warning: there is no permissive policy value and no public pre-parsed entry point, so absence of the argument is the unrestricted state and the pre-parse stays conditional (an unrestricted POST still gets graphql-java’s InvalidSyntax result, not the pipeline’s parse-failure wording); GET is routed with the package-private SPEC_GET and the nullable-guard execute is private, so no routable verb can run with no rule attached; and a policy rejection is not legacy-downgraded, being an HTTP-level rule rather than a GraphQL request error. GraphqlResource is now five annotated methods over the two delegates plus a throwing requireBuiltInEndpoint() gate (throwing because schema() returns String, so no published return type widens for the gate’s sake), backed by a new defaulted SPI toggle GraphitronApplication.builtInEndpointEnabled() for consumers that mount their own path and must not leave the ungated one serving; its javadoc is honest that this is a 404 gate on a still-registered route, not a de-registration. The re-exported GraphqlResource.GRAPHQL_RESPONSE_JSON stays a constant expression (verified in the class file), so consumer @Produces annotations naming it keep compiling. Coverage splits by tier rather than habit: unit-tier OperationGuardTest (12 cases, no container) pins the guard as a decision table over resolveOperation × permits, including the operationName smuggling case in both directions, both unresolvable-fall-through cases, the defensive set copy and all four factory validations; execution-tier MountedEndpointTest (12 cases, over a /env/{callingEnvironment}/graphql fixture shaped like the javadoc example) keeps only what a container shows, the policy on the wire with its status/media-type/message, the same mutation executing on the mount that passes no policy, 405 on GET from both mounts, the legacy non-downgrade, the templated asset base and SDL, request-scope ordering across the delegation, a NotAuthorizedException passthrough, and all five built-in routes answering 404 with the toggle off; OverlappingMountTest (3 cases, neutral /probe pair, no profile and no second boot) pins Jakarta REST’s literal-character root-resource sort that makes a templated mount shadow the built-in sub-paths. The single most load-bearing addition is GraphQLOverHttpConformanceTest.unparseableDocumentIsTheEnginesInvalidSyntaxResult, the only assertion in the tree that can see an unconditional pre-parse: without it an implementer could change the response body of every existing consumer and no test would fail. No existing expectation was edited; the smoke test is untouched. Docs reconciled across every surface the split invalidated (modules.adoc, the example README’s app and testing sections, beans.xml’s comment, `GraphqlRequest’s javadoc pointer, the `graphiql.html head comment, both tools/graphiql-build/README.md occurrences). Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db (0 failures). The review’s retirement sweep found one first-pass survivor the implementer’s list missed, R423’s body still naming the retired GraphqlResource.execute(), repointed at GraphqlHttpHandler in the approval commit; below the recurrence bar for `RetiredVocabularyGuardTest’s registry.

  • R589 (b97b1cb umbrella amendment, 882c037 claim views + conflict detection, fac7704 column-match witness view, 15d44b8 demand/exemption shadow rows, beafe4a carrier split + funnel mints, d20e9cc Conflicted projection, d5cec11 + 3f53bab + 7aefee2 review fixes): classification is a relation; validation adds facts. The intent_ derived stratum arrives: intent_authored_field_claim / intent_authored_type_claim union one arm per claiming graphitron_ relation (position masks in the arms, presence fallbacks where a decode can decline), and AuthoredClaimConflicts, the store’s first reader, replaces the four walk-side conflict sites (reduceDirectiveConflict’s two detector lists, the ad hoc `@service`@mutation+` check, TypeBuilder’s `@table`@error+` check) with one grouping detection per grain; a conflicted coordinate stops tombstoning and keeps its arm-order classification while the violation mints into diagnostics. intent_column_match_claim proves the inferred-claim witness model in shadow; the demand and exemption rules land as rows (intent_*_demand* / intent_*_exemption*, ReachabilityRows materializing the type domain at capture cadence) with corpus agreement against the legacy registry. InputField.ConditionOwnedField splits out of UnboundField (which becomes the genuine-miss carrier, attemptedColumnName non-null), the malformed @condition(override: false) shape mints at the classification funnel keyed by definition and resolving table (closing R221, subsumed, its file deleted with this entry), and the cascade verdict mints use-keyed with the occurrence path (intent_input_occurrence_path* in shadow). FieldClassification.Unclassified splits into Unresolvable and Conflicted(claims, violation): a broken DELETE mutation reads as a DELETE with its intended table on the LSP/MCP surfaces, sourced from decoded slot facts (UnclassifiedField.definition() deleted, containment now structural), with Conflicted edge-bearing through TableClaiming. Accept/reject is unchanged throughout; retired: PairVerdict / pairVerdict / reduceDirectiveConflict, the per-position detector sites, the three-cases-in-one UnboundField reading.

  • R613 (19dec9a capability, be7c128 docs): generated column filters compose beside the lookup VALUES join. The validate-time deferral was stale rather than a design position, so the whole capability is two deletions and no emitter change: ConditionCommands.requireNoGeneratedFilterOnLookup with the lookup boolean that existed only to reach it, and the lookup arm of GraphitronSchemaValidator.validateConditionEmitImplemented (the method survives for the single-table interface child deferral, a genuine emit gap). The same era’s cardinality co-read went with it: validateRootLookup had been OR-ing LookupMapping.ColumnMapping.hasListArg() with any generated filter body param’s list-ness, which since keys are excluded from that filter upstream could only read a non-key argument, and would have become an active false rejection the moment the guard stopped making the co-presence unreachable. The mapping is now the single source of the key-cardinality fact. RootLookupValidationTest’s three deferral rows became the full twelve-cell cube over key list-ness x return list-ness x non-key-filter list-ness; two cells are exactly where the co-read used to flip the verdict. Three additive sibling sakila fixtures pin the three emitters at the execution tier (languagesByKeyGenerated` root with a whole-statement baseline, Film.actorsGenerated inline multiset, Film.actorsBySplitLookupGenerated batched loader, the latter two asserting the rendered predicate and row content since they mint runtime-prefixed aliases), and ConditionCommandsPipelineTest pins the key-not-restated fact at the model. Docs: lookupKey.adoc gains the positive statement plus the empty-key-list and filtered-key facts a migrating consumer needs, and the coordinate-scoped "`@lookupKey` is exempt from the implicit-predicate path" phrasing narrowed to the argument at all three sites. The measurement that the manual asserted a positional contract the generator did not implement was split out as R617 rather than absorbed, and the doc claim about @lookupKey on an individual input field as R614.

  • R603 (79b60a2 core, gate rework 76ac937; re-baseline onto the shipped R610 store d0dec98, Ready d157d0d, In Progress 51b2fc0, In Review 6450d93, gate rework requested 79d05b5, rework In Review eb6a328): The fact store gains its sixth family and its first post-capture oracle writer. javac_diagnostic transcribes a dev-loop compile round in javax.tools.Diagnostic’s own vocabulary, which is the point of the naming: the Backlog framing asked where run-output facts could live given that neither the transcription families nor the reserved `intent_ stratum seemed able to hold them, and the landed doctrine dissolves the question rather than answering it. A compile diagnostic is a transcription like any sql_ or jvm_ row; what distinguishes it is not the kind of row but when its writer can run. So there is no output_ umbrella prefix (a role name is exactly what the naming rule rejects), each post-capture oracle gets its own vocabulary-named family, and the DDL header states cadence as its own axis orthogonal to prefix-picking: a family whose writer runs after capture has its own writer on that writer’s cadence, and capture clears the run’s own graph partition of it before regenerating, because the rows describe an emitted tree the run is about to replace. graph_name leads the natural key (file, line, column, ordinal) with a structural FK to store_graph, inheriting R610’s dimension by definition and passing FactSchemaGateTest’s exemption-polarity gate with no case to make; `kind stays an open column because a CHECK enumerating javac’s enum would be a hand-maintained copy of an externally owned taxonomy, code is the typed dimension a display list never had, and the two key columns transcribe absence as javac’s own sentinels (NOPOS’s `-1, the "(no source)" placeholder) rather than NULL, a primary-key column admitting none, with the departure from the NULL-uniform absence discipline argued in the comment rather than drifted into. CompileDiagnostic reshaped while open: severity renamed to kind (it always held Diagnostic.Kind.name()), a nullable code component added, and from normalises the file through the single canonical-URI site at the javac boundary, so console, workspace and store agree on one spelling by construction instead of each sink normalising or not. The ERROR predicate collapsed onto the record’s own severity() projection, read by CompileRound.errors() and the MCP diagnostics tool alike. CompileFacts is the writer, homed with the round’s producer rather than at the workspace layer because the producer owning its transcription is what makes "one flattening, three sinks" true: it takes the dev session’s live store handle (DevMojo opens one at startup and closes it in cleanup, the contract this item fixed so it and R569 could not land incompatible halves) because for this family the store is a delivery channel, not a cache, and a round written where the reader cannot see it is a round the diagnostics surface answers wrongly about. One transaction per round stands in for a completeness stamp; every statement carries the graph predicate, since in R610’s shared store an unscoped delete is one dev session erasing a sibling module’s diagnostics. StoreRefresh needed no code at all, and the reversal is on the record: R610’s spec framed ownership-scoped refresh as taught-per-relation, but its implementation landed the graph half derivationally, so a relation carrying GRAPH_NAME is ownership-scope-cleared by construction. The agreement driver gained an ORACLE arm for relations a post-capture oracle owns, where no independent second walk can re-derive the verdict without re-running javac; its two anchors are deliberately non-vacuous, a two-graph lifecycle anchor (seeded, so "cleared" is distinguishable from "never written", under two graphs, so "cleared what it owns" is distinguishable from "cleared everything") and a write-read content anchor at the oracle’s cadence. Independent-session In Review → Done review, two cycles. The first pass found the delivery otherwise strong and held on two findings. The Diagnostic.Kind partition test could not fail: against the shipped ternary’s catch-all, both of its assertions are tautologies for any enum constant (enum-name uniqueness making the second one true for every present or future kind), so a new Kind would project to "warning" with the suite still green, while two shipped javadocs asserted that a new kind "fails a build instead of falling through silently": prose claiming the second of the three honest forms in development-principles.adoc § "Documentation names only live tests/code" when the named test did not carry the claim. Separately, CompileFacts.java had raw NUL bytes typed into the ordinal key’s separator char literals, so git classified the item’s central new class as binary: it rendered as "Binary files differ" in its own landing diff and grep degraded to a filename match, blinding every future diff-based review of it. The rework added the golden-set pin on Kind.values() beside the existing loop with a message directing whoever trips it to classify the new kind before widening the pin, and spelled the separator '\0'. Two non-blocking notes were also taken: the minted store_graph anchor’s last_captured now carries a comment saying why the write time is the honest value where no capture ran, and the writer’s javadoc narrows its failure promise to the DataAccessException wrapper it actually catches, stating that anything else is a bug it deliberately does not swallow. Second pass verified both closures at trunk tip rather than on report: full reactor green under mvn install -Plocal-db (14/14 modules), the file confirmed NUL-free and ASCII in the committed blob, and the new pin driven to failure by narrowing the golden set, which produced exactly the intended message. Deliberately deferred with their first consumers: the emitted-file inventory and every other output oracle (execution results, test outcomes). Because this item landed before R569, that item’s compile bridge table is never built; its spec already adopts the fork where the diagnostic view’s compile arm reads javac_diagnostic from day one, and decides there how the arm handles the graph column. R430’s body took the one-line correction that its input is now a URI.

  • R610 (b534810 core, gate rework e344cac; Spec 280e34e after nine revision passes, Ready bc3cdaa, In Progress 945cadc, In Review 652e105, gate rework requested ab51049, rework In Review e6d7fd4): graph_name VARCHAR NOT NULL leads the primary key of every graphql_/graphitron_ base relation (83 of them), so one fact store can hold several graphs without fusing them. The motivation is federation, where User in two subgraphs is deliberately two different types and today’s type-name-only keys would first-wins-merge them into a fictional type or a constraint violation, breaking the split that makes every primary-key violation a capture bug rather than an author error. The dimension ships with its first mechanism rather than ahead of one: the persisted store leaves target/ for the platform’s per-user cache with a per-workspace segment, so a second module of the same workspace puts its graph in the same file the moment it builds. New store_graph anchors the partition (base_dir, build identity, last_captured), the seven parentless family roots take a direct FK to it and everything else reaches it through its existing parent chain, and the graph’s SDL recipe is remembered beside it (store_graph_schema_input / store_graph_schema_extension) so a currency check can re-expand a cold graph’s globs and re-hash its files without building the owning module. jvm_ and sql_ stay graph-free but stop being store-global, leading with their source instead, so two modules' classpaths and catalogs coexist rather than the second build clobbering the first. Capture is graph-scoped through the sink, which stamps the column and namespaces the claim keys so no SDL call site changed; that is the load-bearing half, since claim mirrors every natural key by hand and widening the database keys alone would have relocated the fusion one layer up. Refresh became ownership-scoped: the graph’s own partition clears by graph_name, an owned stale classpath source clears by source, and a source no run named is never examined, because a jar absent from this module’s classpath may be another graph’s live dependency. The store opens in H2 mixed mode so parallel reactor modules attach instead of being handed a cold fallback, which forced three reversals argued on the record: openReadOnly and its copy-to-temp snapshot deleted rather than converted (H2 refuses AUTO_SERVER with ACCESS_MODE_DATA=r), the in-use classifier deleted with it, and a shared store never discarded ; any failure to open or attach falls back to the in-memory store and leaves the file alone, made safe by moving the DDL-hash-and-version stamp into the directory name so an upgrade opens a different file instead of one nobody can use and no run will replace. location() reports where a store landed, which is the only way a test can address a path it cannot name. Consumer surface: <graphName> defaulting to ${project.artifactId} and <storeDirectory> with a graphitron.store.directory property, both documented in mojo-configuration.adoc, plus the workspace resolver that chains siblingModuleBasedirs’ existing one-step aggregator walk to the outermost root (the filesystem aggregator chain, since `MavenSession’s top-level project answers "where was `mvn invoked" and the parent chain is a different graph than the aggregator chain). Three gates joined FactSchemaGateTest, the first two in exemption polarity so a new family is covered by default: leading-key partition dimension, an FK closure from every graph-keyed relation to store_graph, and the two-graph fusion test the motivation promises. Independent-session In Review → Done review, two cycles. The first pass found the delivery otherwise strong and held on two blocking defects, one of which hid the other. The recorded departure sql_referential_constraint.referenced_source_name declares a foreign key that can cross package partitions, while CatalogFactCapture cleared each package’s sql_ partition interleaved with the per-table walk, so a warm refresh over a multi-package catalog deleted one package’s constraints while a not-yet-cleared sibling’s referential rows still pointed at them; measured at the gate against the multischemafixture catalog, not inferred. It had gone unnoticed because FactCapture.run caught DataAccessException and recaptured in memory at debug level, and its javadoc’s claim that this "reproduces a genuine capture bug" does not hold for a warm-only bug whose retry runs cold ; the observable effect being that such a consumer takes the cold path forever while the shared file keeps the first run’s rows, silently. The rework clears every owned package’s referential rows before any owned package’s constraints, over the whole set rather than one package at a time, and retries a failed warm capture once before demoting, logging a deterministic failure at warn with the exception. A retirement-sweep survivor (RewriteContext.withStoreDirectory, whose javadoc still placed the store in the build directory and which had no callers) was deleted, and four smaller gate notes were taken: two self-satisfying assertions in the new gates (store_source/store_stamp compared to themselves, and an FK closure counting graph-free edges), SchemaRecipe.buildFile normalized in its compact constructor to match its own javadoc, and the departures that had shipped documented only in a commit message folded into the item body. Second pass verified each closure at trunk tip rather than on report: full reactor green under mvn install -Plocal-db (14/14 modules), the new WarmStartRefreshTest case drives FactCapture.capture directly so the retry cannot mask a regression and asserts the cross-package row survives, the retirement sweep is clean, and the reactor build writes exactly one workspace segment into ~/.cache/graphitron/model/ with the plugin’s invoker ITs and mojo unit tier pinned under target/. Deliberately deferred with their first readers: store_graph_source membership for the jvm_/sql_ side, eviction (this item writes the last_captured / last_seen stamps it will need), and the freshness check’s driver. R589 and R603 inherit the dimension by definition, and R612 adopts the recipe relations in place without a rekey.

  • R473 (b48b0f8 + 7eb474f, gate rework dd77f66; Spec 11de611 after six revision passes, In Progress 86b4db2, In Review 0087b5b, gate rework requested 6fc2ca1, back to In Review 177e269): An ID-typed field could still acquire node semantics implicitly, with the identity read from table facts (__NODE_TYPE_ID / __NODE_KEY_COLUMNS) rather than from what the author declared, at four coordinates. While that was true the schema did not say what an ID meant, an author could not tell the two readings apart by looking, and BuildContext.resolveDecodeHelperForTable had no safe deletion point. The grammar makes the implicit reading available in exactly two places where it cannot be ambiguous: Node.id on a node type is that node’s identity by construction (already shipped as R580’s carrier, and typeName: is now rejected there as either a restatement or a contradiction), and an argument or input field named for its target’s Node.id is that node id ; the name carries it, and the node comes from the target rather than from a metadata read at the use site. Bare @nodeId generalised to every coordinate as the middle tier ("node id, target inherited"), which the shipped @nodeId documentation already promised and which @node being on OBJECT makes forced rather than convenient; that generalisation dropped the fixture migration from ~25 sites to zero. The argument coordinate resolves through NodeIndex.forName off the unwrapped element type; the input coordinate holds no type name to discard (InputFieldResolver.resolve threads the input type’s own name, and a mutation input has no node type in its SDL at all) so it resolves through NodeIndex.forTable, singleton or a rejection naming typeName:. One shadowing rule at all three coordinates: a directive-less node-id reading colliding with a real column of that name is an error, not a warning and not a contest either reading wins, and it is literally one method (BuildContext.rejectShadowedNodeId, which R580’s shipped output-side rejection delegates to) because divergent wording would be the first step towards divergent semantics. Deleted: all three synthesis arms, resolveDecodeHelperForTable, buildInputNodeIdReference, findGraphQLTypeForTable, findGraphQLTypesForTable and the typeNamesByTableKey index behind it, both shim loggers, CallSiteExtraction.NodeIdDecodeKeys.SkipMismatchedElement with LookupRows’s effective-row machinery, and two shim-intent test classes; `resolveDecodeHelperForType collapses to the forName lookup and returns an Optional. Two departures from the plan that read better than it: the rejections landed classifier-side rather than in GraphitronSchemaValidator (the classified model exposes no schemaType() at those coordinates, and UnclassifiedField already carries the source location the LSP reads, pinned by a test), and rule 6’s key columns come off the classified NodeType rather than the table’s raw metadata, so SDL keeps winning on typeId and key order. Two fixtures turned out to carry @node without implements Node and had never been NodeType`s at all; the retired typeId-suffix fallback had been masking the malformed declarations. sakila needed no migration, exactly as planned: `filmActorByNodeId(id: [ID!]!) is covered by the name-carried reading and its GraphQLQueryTest round-trip stays green. Consumer exposure is two silent narrowings on coordinates preceding column resolution; the user re-confirmed on 2026-08-09 that sis is the only touched subgraph and is migrating it themselves, so neither needed WARN staging. Absorbed the argument arm from R273 and dissolved R27 (see its discard entry above). Independent-session In Review → Done review, two cycles. The first pass found the build green and the test coverage complete but requested rework on four findings, one of them an invariant break: rule 6’s argument arm read its target from fieldDef.getType() while every caller of resolveTableFieldComponents passes the connection-unwrapped element type and a matching rt, so on an @asConnection field the arm compared a wrapped type name against an unwrapped table and never fired ; bazes(id: ID): [Baz!]! rejected as the grammar says while the same field returning BazConnection resolved silently onto the raw id column, which is exactly the coordinate-specific divergence the uniform rule exists to prevent. The other three were prose the flip had made false and left standing: the migrating-from-legacy recipe still listed the shims as WARN-today behaviour in seven places including a migration step telling a consumer to grep the build log for a WARN string the flip deleted, ten sites across main sources and tests still described the deleted arms, and SkipMismatchedElement had lost its last producer while the renderer still switched on it. Second pass verified each closure independently rather than on report: the fix threads the unwrapped name through classifyArguments/classifyArgument (one caller, so the thread covers all ten resolveTableFieldComponents sites), the connection and plain-list spellings now produce byte-identical rejections, the arm is confirmed reachable through the wrapper on a non-shadowing table, and the new pin fails 1-of-38 in its class when the derivation is reverted. Two residuals filed rather than held at the gate: R615 (graphitron-sakila-db’s `init.sql still documents the live idreffixture DDL as existing for the deleted IdReferenceField shim tests, misdescribing a fixture whose actual consumers are now the qualifier-map and @reference(key:) tests) and R616 (the Implementation section’s landed notes cited pre-rebase SHAs absent from history; recorded because the same trap will catch the next item that collapses its plan after a trunk sync).

  • Discarded: Retire @nodeId and IdReferenceField synthesis shims (retire-synthesis-shims, R27): its deletion set is empty. R473 (explicit-nodeid-grammar) deleted all three shim sites in the same motion as the grammar that replaces them: the FK-qualifier IdReferenceField synthesis and the bare same-table arm in BuildContext.classifyInputField, and the non-id bare-scalar-ID arm in FieldBuilder, together with both shim loggers, resolveDecodeHelperForTable and the helpers that died with it. The retirement could not be sequenced the other way round: while a directive-less ID could still mean "node identity, resolved from the table", the table-first helper had no safe deletion point, so the grammar had to land with the deletion rather than after it. This item’s remaining written content was its gate ("sis migrated, plus one external-consumer release window") and its WARN-to-error flip. Neither survives: the user re-confirmed on 2026-08-09 that sis is the only touched subgraph and is migrating it themselves, and there is no WARN left to flip, because the grammar replaced the warned readings with rules rather than with errors at the same coordinates. The migration recipe the item carried lives on in the user manual (docs/manual/how-to/migrating-from-legacy.adoc), which is where a consumer reads it. Re-open trigger: none; a future implicit-nodeId reading would be a new item against the grammar, not a resumption of this one.

  • R580 (cefb16a classifier + 8cf7766 docs, gate rework 47d11a4; re-scoped d9b5616, spec body 81afbee, Spec revisions 9a55d9e`+6b905c1`+b98be81`+3bb3a82, Ready `89fa452, In Progress bd1d6d1, In Review 8ad26c5, gate rework flip 6b2f808`+7eee77b, rework In Review `b886e1c): implements Node declares nodehood and @node supplies or overrides the two identity parameters, so a @table type publishing the Relay contract over a jOOQ class carrying __NODE_TYPE_ID / __NODE_KEY_COLUMNS is a complete node declaration and takes typeId and keyColumns from the catalog. Unchanged on every other axis: @table + metadata without the interface stays a TableType, which is what keeps a nesting projection over a node-bearing table from becoming a second node; explicit @node still wins on the axis it names; @node without the interface is still rejected. The SDL gate is the whole safety argument, because the auto-promotion shim retired from this exact site promoted on metadata alone, and the incident behind its withdrawal was ~200 sis event types over tables sharing __NODE_TYPE_ID = "195" promoting in lockstep and then being symmetrically demoted to UnclassifiedType by the typeId-uniqueness check. The predicate split is the real scope: @node presence and NodeType membership used to be the same set, so several consumers read the directive off SDL and stayed consistent by coincidence, and new NodeDeclaration names the question once for reachability seeding, the arrival fold, federation entity synthesis and the LSP node view. It sits above classification and takes only a JooqCatalog because two of those run before any type is classified. NodeProvenance records per-axis where each identity parameter came from, and earns the slot immediately: the shadowing rejection reads it to point an inferred node at the metadata and a declared one at the declaration. Riding along is a correctness fix on the explicit @node path, not a compatibility rider: a node type whose table also has a column literally named id silently published the raw column as its Relay global id, while the identical declaration over a non-colliding table published an encoded one, and nothing warned. That shipped first as warn-and-flip and was revised at the gate to a rejection naming both remedies (@nodeId for the global id, @field(name:) for the column), because a warning still picks one of two legitimate readings and only narrates the pick, while the reading it picks changes the wire value; the input and argument coordinates make the same call, so the two had to agree. LintRule.NODE_ID_SHADOWS_COLUMN was removed rather than kept for the Source.CLASSIFIER census, since LintRule.ids() is the namespace a consumer’s <lint> config validates against and a rule that can never fire is not configurable. Fixture cost measured rather than estimated: 37 sites across 7 test classes gained an explicit @nodeId or @field, each behaviour-preserving against the shipped warning, and none in graphitron-sakila-example. The hoisted arm is pinned to the Node-interface id field rather than the shim’s any-bare-ID predicate, which would have rerouted externalId: ID on a node type from its column to a nodeId encode. Both decode-helper collision axes are pinned, including the quieter zero-to-one direction, where adding implements Node to an output type changes an input leaf’s emitted helper from the typeId-suffixed fallback to the type-name-keyed one. The execution-tier proof is the sakila example’s FilmActor converted to the inferred spelling rather than a variant added, so the existing filmActorByNodeId round-trip proves it through real generated code, real encode/decode and a real database. Two departures, both narrower than planned: one lint finding rather than two, because neither silencer’s LintFix is attachable (both are directive insertions after the field’s type, and graphql-java records a type node’s start location but not its end); and the decode-helper one-to-two transition is only half reachable since R581 made a call site holding an authoritative type name resolve by name. Deliberately not done: no ClassifiedCorpus sibling (the verdicts are pinned by NodeInferencePipelineTest, and a corpus example is a documentation deliverable rather than missing coverage), and the lint-rule inventory page filed as R592. Independent-session In Review → Done review, two cycles. The first pass found the delivery otherwise exactly as planned and held on a docs finding the plan’s own census had missed: migrating-from-legacy.adoc still asserted the pre-inference rule as current, on the page whose audience is precisely the metadata-carrying legacy population inference exists for. The rework closed it at the heading, the rule paragraph and the summary checklist, added the shadowed-column failure authors on that path will meet, fixed code-generation-triggers.adoc’s prose and variant-table row, and opened a `Named structural errors section in the diagnostics glossary with the drift-protection paragraph amended to keep DiagnosticsDocCoverageTest’s bidirectional guarantee honest. Second pass verified the closure and the delivery at trunk tip rather than on report: full reactor green under `-Plocal-db (14/14 modules, including the sakila PostgreSQL execution tier and the docs render), NodeInferencePipelineTest 37/37, the user-facing-doc check clean across all five touched manual pages, and a retirement sweep run although none was declared, finding the retired lint names only in roadmap history. The sis census was never performed and remains this item’s one unverified safety claim. It cannot be answered from this repository and implementation proceeded with it open on the user’s direction, so it converts to a pre-rollout check rather than lapsing: enumerate consumer object types carrying implements Node and @table and no @node, grouped by the backing table’s __NODE_TYPE_ID, and confirm every group is a singleton and no member’s id is @field-pinned to a non-key column. If any group has more than one member, this change produces a diagnostic per member of that group, and the mitigation is @node(typeId:) on one side, which the collision message names. Three nits were fixed in this closing commit rather than sent back for a cycle: NodeDeclaration’s class javadoc claimed the classifier gate calls the predicate instance and that the two "cannot disagree", when the gate re-derives the conjunction from the shared static helpers because it needs the metadata values anyway, and the two deliberately part on `@node without implements Node (a declaration-level true, a classifier rejection, contained because the type fails the build either way); and two inbound roadmap citations naming this plan page by path were repointed here, which is R596’s 404-on-delete census. The reviewer landed the javadoc correction, so that one-sentence edit has the same session on both sides.

  • R587 (be60eba core, 81d5073 gate rework; filed 453b916, spec revision 819da51, Ready f3f9faa, In Progress 18c8842, In Review 1db2240, gate rework 61b95d6, rework In Review 00d8c18): Roadmap markdown code spans render inert, so a quoted macro publishes as typed. A markdown code span is literal by definition and a single-backtick AsciiDoc span is not, so the md-to-adoc render published a live link for every backticked xref and resolved, or WARNed on, every backticked attribute reference, invisibly from the markdown source; the only workaround was knowing AsciiDoc passthrough syntax while authoring markdown, which adoc-xref-section-anchor-gate.md had taken up 17 times. New InertSpans owns the vocabulary as one unit: plusFormFits is the single predicate both producers read, so the readable `+content+` form, the ... fallback for content carrying a plus, a backtick or edge whitespace, and the attrlist-safe label producer cannot drift apart, and the recognizer the corpus gate reads is that same definition rather than a second list. The structural half is the payoff: Main’s four hand-composed inline chains (heading titles, body lines, table cells, and a no-op stub for status-board backlog descriptions) collapse into one `inlineMdToAdoc pipeline that the fifth markdown-sourced surface, front-matter titles, also routes through, with one named helper per sink (titleHeading, titleCell, titleLabel). Two mechanics implementation had to settle: spans are held behind a placeholder rather than split out of the text, because bold wrapping a span and a link label carrying one are both common in the corpus and neither pattern matches once the construct is cut in half; and the converter carries the open span delimiter across lines within a paragraph, without which a line that closes one hard-wrapped span and opens another pairs the closer with the opener and wraps the prose between them. GeneratedAdocSpanGateTest renders the real corpus through the real renderers and fails on any monospace span outside listing, literal, comment or passthrough blocks that is not inert; table blocks are deliberately in scope, since the cell is one of the policed surfaces. Retired: Main.escapeAdocInline, and the hand-written plus-delimited passthrough as a roadmap authoring convention (the 17 sites are back to plain backticks and R582’s body is reconciled). Independent-session In Review → Done review, two cycles. The first pass found the mechanism correct and held on the retirement sweep: R582’s body still stated the pre-change world in the present tense at three sites and described the retired hand-written workaround as live practice, so the file contradicted its own reconciled Design note, and fixing it after approval would have meant fixing it with the Retired vocabulary declaration already deleted. Five non-blocking notes travelled with the rework request and were all taken: the by-theme :description: dropped its span (an attribute entry takes the header substitution group, so nothing was live there and a plain-text meta description is the right shape for the sink), the em-dash sweep moved above the link rewrite that releases label spans early, titleLabel dropped the pipe escape it had inherited from a cell context it no longer serves, titleLabel_bareResidue_failsTheCorpusGate pinned the enforcement half of the label contract, and one over-width line was rewrapped. The second pass verified the reconciliation and each note against the staged corpus rather than on report: zero hold-placeholder leaks, zero unconverted markdown links (so the carried-delimiter state cost no link rewrite anywhere), em dashes 700 in source to 9 staged and bold 1665 to 4 with every survivor legitimately inside a span or in a hand-authored copy, and the meta description now plain text. Full reactor green under -Plocal-db at both tips; 31 tests across the two new classes, both in the base build.

  • Discarded: Dimensional model pivot: slots over cross-product permits (dimensional-model-pivot, R222): retired 2026-08-06 as the fact-base architecture’s board sweep landed (roadmap/audits/2026-08-06-fact-base-impact-sweep.md). The umbrella’s thesis won one layer below where it was looking: axes-as-slots is delivered as per-directive intent_ relations and the claim view’s arm list (R589 over the R595 store), while two of its three load-bearing claims were inverted (producers read captured rows, not graphql-java primitives; validity is violation rows, not a WalkerResult wrapper), its operation axis was delivered by R563 as OperationMember, its input-side premise deleted by R519, and its stage plan replaced by the strangler frame. Residues cut before discard: R601 (unified-diagnostic-stream) and R602 (input-object-directive-locations); the Stage 5/6 legacy-permit deletion inventory migrated into R333’s What dissolves with the Stage 6 end state restated as deletion, and R411 dropped its carve-out dependency. The absorption ledger and rejected design attempts are preserved in roadmap/audits/2026-08-06-r222-lineage.md.

  • R584 (076e9cc; filed 3830a04, spec body e430740, Spec 4e40d1d, Spec revise 2497c0c`+f91f027, Ready `d3f1524, In Progress 4ca7647, In Review 154ff7f): The MCP handshake instructions string routes an agent to every advertised tool family instead of one. The shipped file spent a full paragraph on catalog.tables / catalog.describe and never named the other ten tools or the directives resource, so agents re-derived from text what the wire already carried: shelling out to python3 to count diagnostics whose pre-paging total was in the tool’s own summary line, and grepping a whole monorepo for DELETE mutations that schema already classifies with a resolved table. The fix is a decision about what the slot is for rather than a longer list. The instructions string is ambient (received once at initialize, held for the session, charged on every request); a tool description is local. That split assigns the work: ambient carries only what no single description can say, which is the mapping from an agent’s question to a tool, plus the conventions holding across tools. The old catalog paragraph inverted it, restating catalog.describe’s own jOOQ-comments caveat and `catalog.tables’s "SQL names drive discovery" almost verbatim; cutting that duplication is what pays for the routing table. What ships is a question-keyed routing table over all twelve tools plus the resource, ordered by the authoring loop rather than registration order, and three cross-tool conventions each drawn from an observed miss (the paged-first-line total, IDs stable and shared across tools, every tool reads the live build and four report snapshot freshness). The string is composed rather than one fixed resource ; base block plus `mcp/instructions-execute.txt appended exactly when executeConfig != null ; mirroring execute’s conditional registration, which is what lets the coverage pin assert agreement per boot rather than over a union and lets the exempt set ship genuinely empty. New `ServerInstructionsTest holds three assertions over one subject: a bidirectional coverage pin (advertised-is-routed, routed-is-advertised) deriving the advertised surface from a booted server via listTools/listResources/listPrompts rather than a hand-written list, asserted against the ambient string per boot and against docs/manual/how-to/mcp-agent-context.adoc at a different grain (presence over the whole document, staleness over the tool table’s first column, since the manual is per-tool where the ambient block is per-question); a pin driving all six McpWire.page tools at limit=1 and asserting the first line carries the unpaged total; and a 3600-character ceiling on the composed string, guarding a per-request cost against unbounded append. Presence is checked in backticked form because the bare word "schema" runs through the prose and a substring check would pass vacuously on the very tool the second episode is about; the reverse direction uses a leading-lowercase-letter token rule ([a-z][a-z0-9]*(?:\.[a-z0-9]+)* matched in full) so Type.field, .graphqls, mvn graphitron:dev and fqcn#method/arity do not read as tool claims, with no ignore list. GraphitronMcpServerTest gains the composed-arm case; its existing verbatim initializeReturnsBundledInstructions now pins the base arm unchanged. The manual needed no rewording: it already named every advertised name. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db (13/13 modules), no code-string assertions on generated method bodies, and all three pin directions independently mutation-checked at the gate rather than taken on report ; dropping catalog.describe and records from the prose fails the forward direction naming both, inventing a diagnostics.aggregate routing line fails the reverse, renaming edges in the manual fails its forward half, and making the composition unconditional fails the no-database boot on execute, which is the mutation a union check would have passed. Measurements verified against the shipped files: 411 words / 2564 characters base, 452 / 2817 composed. One residual filed rather than held at the gate: R598 (the warm-degradation messages of docs.search / catalog.search carry no tool prefix or count, which is why convention 1 is scoped to the paged tools). R569 needs no depends-on edge ; registering diagnostics.aggregate cannot land without its routing line and manual row, because the coverage pin fails until both exist.

  • R563 (slices cba24d0/f9f1899/5b78a5a/711933e`+702c7f8/7bb9794`+4546534/98e3623`+043f13e/b1ee673`+030ccf9/9e46d52`+b71a203/850ba5f`+ea5f9db/827a140, rework 608fd85; Spec f107ce4, Ready bf0d649, In Progress 72421e9, In Review e7c420c, gate rework 21b0b699`+bbd829a6, rework In Review `63e164d8): Operations became a relation and the leaf cross-product dissolved additively. coordinate -> operation is the one genuinely multi-valued relation in the classification model, and storing it as a single-valued Operation arm plus leaf identity was the 1NF fault that multiplied leaves: a filtered lookup silently dropped three facts off the operation axis (all three lookup leaves carried filters/orderBy/pagination while their operation() switch mapped them to a Lookup arm with one slot), Paginate fused three verbs into one record, a DML coordinate performed two operations and the model could name only the materialized one, and both structural Operation consumers carried an explicit fallback around the arm for polymorphic filters. Eight slices, each shipping to trunk on its own and the 4-through-7 ones landing as additive-then-destructive halves so each equality is demonstrated in history rather than asserted after the fact: the sealed OperationMember family plus the member view derived from a compile-total 51-leaf crosswalk; the instruments (LeafRatchetTest, the member-grain census); the keystone, where five per-trigger FactVisitor`s joined the pagination precedent and the view re-sourced onto the gathered slots under a canonical-RecordComponent` agreement pin; the tenant re-source and the retirement of OutputField.operation() with its three leaf-identity switches; the back-half re-source, which materialized DeliveryFact and collapsed LauncherCommands.MINTING_KINDS onto a sealed launch verdict; three dissolution slices (the lookup triplet onto their fetch siblings behind a sealed LookupResolution, the DML verb split onto the sealed Write.Dml payload with the dialect requirement becoming a derived accessor, the pivot and routine operation halves); the corpus voice, where @classified(operation:) became a required arm-grain operations: multiset and the 17-arm Operation seal retired outright; and the obligation re-typing. Output leaves fell 51 to 40 (query 12→10, mutation 15→8, child 24→22) with a history line per move, plan leaf references 156 to 124, generator pins 71+76 to 69+59, and LeafReconstructionKeyTest now enforces the acceptance claim leaf = f(source, delivery, target) as a declared triple per surviving leaf, pinned total against GeneratorCoverageTest.sealedLeaves with a collision check whose sole tolerated pair is the single/bulk DML carriers. Emit byte-identical through every slice. Independent-session In Review → Done review, two cycles. The first pass found the architecture sound but the documentation tail broken, and requested rework on four findings: the generated migration fragment had been regenerated from a partial classifier trace at slice 6c and advertised four shipped shapes (MutationServiceTableField, JooqRecordInputType, JooqRecordType, UnclassifiedType) as (not yet supported) on the user manual’s migration page; code-generation-triggers.adoc still named the three leaves 6a retired as live classification verdicts across five tables, though slice 6b had fixed that same file’s DML rows one slice later; LookupValuesJoinEmitter survived in four test-prose sites including a dangling test-source {@link} that no gate catches; and one retired-vocabulary row described a re-grain as a deletion. Slice 8’s claim that the docs/ AsciiDoc audit found zero survivals was corrected in place rather than deleted, so the record now says what the sweep missed. Second pass verified each closure independently: the fragment regenerates byte-identical from a full-reactor trace, the swept doc rows name the surviving leaves plus the keyed lookup arm and the TableTargetField census matches the seal’s four permits exactly, LookupValuesJoinEmitter has zero occurrences outside LeafRatchetTest’s deliberate history lines, and the four test edits are comment-only. Trunk was red from R580’s `NodeInferencePipelineTest when the gate closed; R563’s own In Review tip was built in an isolated worktree and is green 13/13, and the failing class does not exist at that tip. Three residuals filed rather than held at the gate: R590 (the migration fragment carries no --verify execution, so the partial-trace footgun that fired twice in this programme is still unguarded), R591 (SqlGeneratingField remains the storage home for the condition, orderBy and paginate payloads, so on those three axes the member rows are a view over the leaf rather than the reverse), and R586 (the exemption-taxonomy arm census). R222’s operation-axis content and R543’s parked corpus edits are discharged; R333’s stay-Ready call belongs to its own gate.

  • R579 (59313d7; Spec 4e9c30d, Ready 6e85bd9, In Review f5acb32): Dropped the dead parentTypeName parameter from FieldBuilder.parseExternalRef and followed the orphaned value one frame up, off ExternalFieldDirectiveResolver.resolve as well, since that call site was its only reader. Four removal-only edits, no logic moved: the cascade terminates in ServiceDirectiveResolver.resolve and FieldBuilder.classifyChildFieldOnTableType, where the name is genuinely live (parent-table validation and return-type projection on one side, UnclassifiedField/ComputedField construction on the other). No tests, by design: a signature narrowing has no behavioural axis, so the compiler plus the existing @service and @externalField pipeline coverage is the whole verification story, and a test here would pin an arity rather than a behaviour. Independent-session In Review → Done review: full reactor green under -Plocal-db (13/13 modules), the sweep confirmed complete and correctly terminated (one call site each, both terminal frames still read the name, no orphaned @param javadoc, and every sibling resolver that keeps a leading parentTypeName was checked to actually read it, so no parallel dead parameter escaped). The user-facing-doc check and retirement sweep both skip: no docs/ changes, nothing retired.

  • R581 (c87667b): @nodeId(typeName:) resolves off the named type instead of reverse-mapping its backing table. Declaring a second @node over a table broke every @nodeId(typeName:) leaf already pointing at that table, including the leaves that named their type and were therefore never ambiguous: NodeIdLeafResolver.resolve settled the type name, derived the target table from it, then asked BuildContext.resolveDecodeHelperForTable to map that table back to a NodeType, and that reverse lookup answers "zero or multiple" as soon as a second @node covers the table. Reported from the sis / utdanningsregisteret federation work, where the shared Organisasjon entity has to live alongside URegOrganisasjon over one table; the second type needed no reference from any query, field or input, and declaring it was enough. BuildContext.resolveDecodeHelperForType is the new name-first entry point (NodeIndex.forName when a NodeType carries the name, the table-keyed helper only for the orphan case it exists for), consumed by NodeIdLeafResolver.resolve and buildInputNodeIdReference; the call sites that genuinely hold only a table name (the synthesis shims, the bare-scalar-ID argument arm in FieldBuilder.classifyArgument) keep the table-keyed helper, and the resolver’s surviving rejection is re-aimed at what is actually wrong at that point. resolveTargetKeys reorders the same way, so a named leaf takes typeId and keyColumns from the type’s own @node rather than from the table’s KjerneJooqGenerator metadata; the old order discarded the per-type reconciliation TypeBuilder performs, which is what lets SDL win on typeId outright and on keyColumns order. This was R473’s phase 1, carved out and driven by the field report; R473 keeps the rest of its grammar and its phase 2 shim retirement. Independent-session In Review → Done review: full reactor green under -Plocal-db (13/13 modules), no code-string assertions in the delivered tests, and each half of the production change empirically confirmed to be load-bearing by reverting it against the whole graphitron module (reverting the name-first decode resolution fails the three new multi-@node cases; reverting the resolveTargetKeys order fails the key-order case, and only that one out of 3120 tests). The manual’s new "Several node types over one table" shape was also run through the classifier at the gate rather than trusted as prose: both types classify as NodeType, both id: ID! @nodeId fields classify, and GraphitronSchemaValidator reports nothing. Two findings filed as follow-ups rather than held at the gate: R583 (the typeId axis of the resolveTargetKeys reorder is unpinned, and it is the wire-format prefix resolveNodeIdRecordDecode feeds into decodeValues on the jOOQ-record input-bean path) and R582 (nodeId.adoc’s new cross-reference targets the AsciiDoc default underscore anchor while the site renders kebab-case section ids, so the link lands at the top of `node.adoc instead of the section; two older instances of the same mistake exist, which is the argument for a gate rather than a sweep). Process note: the item was created and reached In Review in the implementation commit, so it never took a Spec → Ready sign-off.

  • R566 (e54be59 core, 870bcd1 sweep, cd8538f acceptance fixture, c757b0a verb rebase; Spec e31c51e, revisions b77a5f4`+82a0dc0`+43e12c7, Ready 1539675, In Progress edfb780, In Review 88e7779): Reopened the @table-on-input deprecation window. The cutover that made the location a classify-time rejection did it in one step, with no build that merely warned in between, so a downstream subgraph picking up a newer 10-SNAPSHOT hit six type-level rejections at once. TypeBuilder.buildInputType now falls straight through to the plain path and the deprecation is announced per usage as a BuildWarning.NoRule advisory that says the directive was ignored, not only that it is deprecated: an input whose @table named a table other than the one its consumer resolves is discarded rather than migrated, and the message is the entire mitigation. Dropping the verdict was necessary but not sufficient. Four main-source readers of raw @table on an INPUT_OBJECT were harmless only because the rejection short-circuited them and all four went live the moment the input classified: RecordBindingResolver.groundRootProducers (an input-axis ProducerBinding.RootTable that honored the directive, deciding the backing carrier, and folded against a @service param’s own observation into a RecordBindingMultiProducer rejection, so the build still failed with a backing-class diagnostic instead of a migration message), InputBeanResolver.collectJooqBindings (nested @table rejected as a second DML target), BuildContext.classifyInputField (nesting descent gated on the directive’s absence, so a nested group fell through to the column-lookup path and resolved as a column named after the nested type), and MutationInputResolver.rejectInputFieldDirectives (recursion gated the same way; this one failed open, letting a @condition buried inside a nested @table group escape an admission scan its directiveless twin trips, with no build error). All four deleted, so inertness is uniform. The per-verb replacement wording is read off OperationMember.Write.Dml rather than off the mutation field leaves the pre-cutover version enumerated, which the trunk DML verb split had dissolved: one exhaustive switch over the four sealed arms replaces three leaf-enumerating helpers and the compiler now checks that a new verb cannot ship without wording (UPSERT got an arm rather than the spec’s argument that it was unauthorable). Emitted post-classification, not from buildInputType, so warning multiplicity is not a function of lookAheadVerdict memo timing; fix-less by design, since NoRule carries no fix field and LintFix.deleteBareAppliedDirective gates on the definition declaring no arguments. Coverage: TableOnInputDeprecationWarningTest (all four wordings, per-input not per-consumer multiplicity, unresolvable table name inert, declared-table-disagrees-with-consumer, and one whole-schema acceptance fixture asserting six @table-carrying inputs across every verb and nesting classify with warnings and no rejected type or field), the new TableOnInputIgnoredNestingTest for the two nesting-path deletions plus the fail-open @condition site, the service-param and nested-input rejection cases in JooqRecordServiceParamPipelineTest rewritten as equivalence assertions against their directiveless twins, RecordDirectiveIgnoredWarningTest’s record-shadowing case given a producer so the `OBJECT-only guard is actually exercised (a filter-only input returns at !reachable first and would have passed for the wrong reason), and graphitron-sakila-example’s `CityCountryFilter carrying @table again as the end-to-end proof through the plugin, with FixtureWarningsGateTest asserting both advisories explicitly rather than carving the new one out. The retirement statement was reworded off the SDL descriptions and twelve doc pages, including two that were false rather than stale (argument-resolution.adoc’s “buildInputType rejects any input carrying @table”, `code-generation-triggers.adoc’s `UnclassifiedType + "build fails" row), directives.graphqls’s `@mutation(table:) outranking claim (nothing to outrank), and migrating-from-legacy.adoc, where the location moved out from under == Hard removals into a retitled == WARN today, error later bucket and migration step 1 stopped claiming the build will not compile. R520’s LSP sub-goal is void while the window is open and was re-scoped to documentation-only. Explicitly out of scope: the eventual re-removal, which is a fresh item, and the lesson is that the warning has to ship in a release consumers actually built against first. Independent-session In Review → Done review: full reactor green under -Plocal-db (13/13 modules), retirement sweep clean (no surviving _retired_on_input_types anchor, TABLE_ON_INPUT_RETIRED case, _rejectsAtTheType method suffix, or "outranked"), doc sweep re-run with the spec’s generating grep, and each of the three nesting assertions empirically confirmed to fail with its corresponding guard reinstated (including the @lookupKey case’s narrower claim that it survives the recursion gate alone). Follow-up R574 filed for four residuals the acceptance criterion did not reach: an unused DIR_TABLE static import in InputBeanResolver, two table.adoc Constraints bullets that read unqualified but now hold only on OBJECT/INTERFACE, and migrating-from-legacy.adoc’s intro taxonomy still calling the WARN bucket "the synthesis shims". Process note: the spec body was never updated during In Progress (only `status: moved), so three delivered divergences from the plan (the InputBeanResolver design fork, the verb-switch rebuild, the migration-doc retitle-and-move choice) are recorded in the commit messages and here rather than in the plan.

  • R564 (14e0442; Spec ea87dc1, plan body a26a5bd, provenance corrections 2376e92`+cbc11bc, Ready `d92af4c): The javadoc {@link} reference gate survives an out-of-range project.build.outputTimestamp. maven-javadoc-plugin 3.12.0 bundles maven-archiver 3.6.4, whose parseBuildOutputTimestamp range-checks against a 1980-01-01T00:00:02Z floor and throws two seconds under it on SOURCE_DATE_EPOCH=315532800, the ZIP epoch NixOS exports from stdenv; the property is never set here, but maven-archiver falls back to that environment variable, so the check-link-references execution died before doing any of its own work and every NixOS contributor lost the whole build to it on a clean checkout. Fixed by overriding the plugin’s bundled maven-archiver to 3.6.5 (which dropped the range check; not 3.6.6, which raises its maven-core baseline to 3.9.12) in the root pom’s pluginManagement, so the release profile’s attach-javadocs jar goal is covered too. Guarded behaviourally rather than by asserting the XML exists: rewrite-build.yml’s reactor build step exports the offending value, so dropping the override reddens the build on the commit that drops it. Declaring `project.build.outputTimestamp was rejected as the fix: it would work by overriding what the environment asked for and would commit the project to a Reproducible Builds policy as a side effect of a bug fix. Independent-session In Review → Done review confirmed the mechanism at the bytecode level (3.6.4 carries the DATE_MIN/DATE_MAX throw, 3.6.5’s numeric branch returns Instant.ofEpochSecond unchecked and still reads the variable), reproduced the failure on the pre-fix tree and its absence after, verified a planted dangling {@link} still fails the gate under the override so the fix does not buy green by weakening what the gate checks, and ran the full reactor green under -Plocal-db (13/13 modules).

  • R543 (dfb06d6, binding design record e26e18d; Spec 664b37e, revise 3cf90c3`+1a6fd74, Ready `0650a27, In Progress 74dc897, In Review d82b734): The corpus asserts a coordinate’s launcher commitment (R549 slice 8): @commits(source:, result:) lands in the corpus prelude, keyed by coordinate alone like the launcher relation, agreement-checked per coordinate against the relation the harness produces for every example under one stated canonical run configuration, with the outcome typed produced-or-failed (never thrown through the sweep) and the two-entry failure roster ({record-method, service}, the recorded validator-mirror gaps) bound by set equality in both directions. The directive carries arm tokens only, never emitted names, and makes no membership claim; the LauncherSource/LauncherResult prelude enums are mirror-tested against the sealed arm sets. Membership went producer-declared per the Spec’s review round 1: LauncherCommands.MINTING_KINDS, a sealed Leaf/DmlReturn key (the DML family’s membership is return-arm-conditioned, a census-corrected refinement of the Spec’s flat leaf set) with the single covers accessor owning the conjunction; mintedMethodOf and produceWithoutSchema re-sourced onto it, LauncherMembershipTest binds declared to observed minting in both directions over a fixture reaching all thirteen kinds, and the closure test’s hand-maintained twelve-arm isCoveredFamilyMember switch retired. The invocation axis is not an argument (the axis census showed the source arm determines it): INVOCATION_BY_SOURCE is total producer data, pinned at the membership fixture, the closure run and the corpus sweep via the shared LauncherAxisPins. Deliberate absences keep explicit negatives: the root @service passthrough pin stays, the batched polymorphic pair pin was added (the Spec’s premise that one existed was confirmed false; the fixture now instantiates the interface half), and an Encoded-DML pin grounded on the return-arm fact and covers rather than emitsKeyedReQuery() (true for an Encoded DELETE while no row mints; that predicate’s javadoc consumer list corrected). Coverage: the LAUNCHER_COMMITMENT obligation registered in obligations() and corpusObligations(), all 15 LaunchSource`+ResultShape` arms reached by declared-and-agreeing rows with zero exemptions, three new examples (table service child, scalar service child, discriminated-return mutation) reaching the previously unreached arms, and LoaderDelegated’s biconditional entailment stated on the row. `QueryViewRenderer.INTERNAL_DIRECTIVES derived from the parsed prelude (the latent @synthesises leak closed by construction); the Count/Facet exemptions re-anchored on the live ground (synthesised connection fields are not classified coordinates, R562 owns the model question); a launcher-arm occupancy census landed beside the classification one so the next re-measurement is a test run. Emit-neutral (main-source changes limited to the producer’s declaration data and javadoc corrections; sakila expectations byte-identical), PLAN_LEAF_REFERENCES ratchet 144→156 with its history line. Independent-session In Review → Done review re-verified the acceptance criteria against the diff, ran the full reactor green under -Plocal-db (13/13 modules), and swept the one stale carrier-fork sentence off R562’s body in the approval commit.

  • R541 (slices b7032ca plain root, d3178e8 connection non-faceted, 395b4c4 facet plan + legacy connection builder retired, 8ffe4c0 fanned root + invocation slot, dfc1df9 routine root + source axis, 23415d1 discriminated arm + interface root, 1c88a86 lookup pin freeze, 49de8cb lookup fold closes the migration; In Progress → In Review 9b292ea): Root query launcher, the root SELECT family as launcher commands (R549 slice 3c, the second proof of the command architecture). Every covered root SELECT coordinate (plain single/list, connection faceted and non-faceted, tenant-fanned, routine, single-table-interface, lookup) is produced as a LauncherCommand row keyed by (coordinate, operation) and rendered by RootLauncherRenderer, a total function over the command taking no schema; the root fetchers reduced to thin entry points routing on row presence, and TypeFetcherGenerator shed its five inline root SQL builders (~1000 lines). Membership is one total switch (LauncherCommands.rowOf, no default), so the compiler is the membership enforcer and the migration dial deleted on the closing slice. The exact-SQL equivalence pins (RootLauncherSqlBaselineTest plus the faceted pin in the condition baseline and the per-tenant recording-proxy pin in the fan-out suite) were authored pre-cutover and held byte-identical throughout; the frozen-strings rule held (pins only ever added). Second-proof findings recorded in the slice logs: the extras slot dissolved (cursor columns are a correlated view of the ordering slot), the spec’s payload-free-enum premise failed twice (ResultShape and Invocation both promoted to sealed with their first payload-bearing arm), the strategy decides the unit’s interface (the fanned launcher is env-only), and the source axis (LaunchSource) absorbed table and projection rather than sitting beside them. Cross-family handshake with R552 completed (where glue refs and facet fragment refs cross the seam as data). Validator-mirror gaps surfaced but not owned filed as roadmap/root-family-validator-mirror-gaps.md. Independent-session In Review → Done review; full reactor green under -Plocal-db, 13/13 modules; retirement sweep applied in the approval commit (four stale prose references repointed, one Backlog item body refreshed).

  • R552 (4d5f48d slice 1, 7cac278 slices 2+3, slice 4 with R541’s b7032ca`+395b4c4; Spec revisions `4d6e15a`+d1e22d8, Ready `61fde1f, In Progress 1ed6f68, In Review cd949ec): The WHERE family as a coordinate-keyed command relation, the third proof of the command architecture (value-shaped unit, cross-kind launcher edge, external-callee edges, the type-keyed GROUP BY exemplar) and the first family to land. One row per covered (coordinate, resolvedTable) key (participants expanded, nested coordinates recursed, lookup keys excluded by the fact), each carrying its ordered predicate list (Generated terms rendered in the glue body, Authored predicates as calls into developer code, reach as producer-proven FkHop lists replacing four render-time throws), its total glue UnitMethodRef, shared-outer lifts and facet fragments as data with the fragment partition and local-name collision-freedom as compact-constructor failures. render/ConditionGlueRenderer replaced QueryConditionsGenerator with single-layer map-taking bodies ((JooqTable, Map<String, Object>), env appended exactly when a binding reads the request context), one producer-named typed local per argument; every WHERE consumer (root fetchers, split rows methods, lookup rows method, inline $fields arms, polymorphic branch folds, the facet-plan block) now emits the one-line ConditionGlueCall, and FkTargetConditionEmitter deleted in full, so no host can compose a fold inline again ; the structural convergence enforcer. The entity layer (TypeConditionsGenerator, the <ReturnType>Conditions/<Participant>Conditions classes, the WhereFilter naming facts) retired with zero callers; slices 2 and 3 merged deliberately so the dual-scheme window and its merge machinery never got built, with EmissionLog’s duplicate-landing-address hard failure kept as the general backstop. `@condition(contextArguments:) implemented rather than rejected (the inline sites had shipped the shim’s missing-helper bug a second time; the glue class owns its graphitronContext(env) helper through a declared-drain collector), R475 dissolved (producer-named locals, no generated parameter list to collide; fixture compiles and executes), R472 closed via the two-step (deferred rejection, then the nested row when nesting types became walkable), R334’s condition-family ternary chains gone by construction, R387 completed by its subject’s retirement. Borrowed-vocabulary verdict for the programme: the borrow held, nothing copied into command (ArgBinding, OuterLift, UnitMethodRef are borrow-plus-producer-data shapes; PackageImportDirectionTest’s borrow dial plus an eighteen-type closure census guard the boundary). SQL equivalence held across all three landings: seven `ConditionSqlBaselineTest whole-statement pins byte-identical, the one sanctioned delta (runtime-prefixed EXISTS aliases) decided before any pin was authored; ConditionMembershipTest pins relation key-set equals an independently derived covered set. Launcher handshake: LauncherCommand.where is a GlueCall copied off the row, facet fragment refs cross into R541’s carrier plan cross-checked at production. Ratchets across the item: entry points 24→22, generator leaf instanceof 104→97, case 89→87. Independent-session In Review → Done review (this entry’s landing commit): full reactor green under -Plocal-db, retirement sweep swept one surviving computeLiftedOuters javadoc cite and the closed window’s prose off EmissionLog, R333’s row 5 and crosswalk row updated with the landed verdict, R475/R387 files deleted, follow-up R561 filed for the pre-existing body-string scans ConditionGluePipelineTest carried through its rename.

  • R551 (16e45d1, in-review amendment 11947dd; Backlog → Spec da4b5a4, Spec → Ready 47f3e3c, Ready → In Progress c7dd7ae, shipped-at notes e12d2e0`+b88e509`+4077461, In Progress → In Review 39ba5e9): Restated both srp hand-off templates as goal + hard invariants + pointers, dropping the machinery that was shaping reviews into rubric-filling. The two emitted prompts each carried a mandated "Read first (in this order)" sequence, a five-bullet "What to assess" rubric, and a per-finding output format (summary line / location / principle / fix shape); the observed effect was reviews shaped like the rubric rather than like the item, one paragraph per bullet with findings stretched to fill the agenda and nothing surfaced outside it. Current Anthropic guidance for this model generation names that shape as counterproductive: delegate rather than dictate, since a prescribed step sequence fights an agentic loop where each step is chosen from what the previous one revealed; delete instructions rather than add them, since over-specification measurably degrades output; and match freedom to fragility, keeping low degrees of freedom for fragile mechanical steps and high degrees for judgment work. The split the rewrite draws is exactly that line. What a fresh reviewer cannot infer stays prescriptive and survives verbatim in both templates: the sync-first commands with the reason they exist, the reviewer rule with its session-ID trailer convention and git-author fallback for pre-trailer commits, the two-outcome contract with the state-machine action attached to each outcome, and the project facts no reviewer can derive from the tree (where "good" is defined, the stale-reference rule with its FQN-aware-grep hint, the ban on code-string assertions over generated method bodies, and build-green under -Plocal-db as an approval precondition). What is judgment work loses its scaffolding: the reading order becomes an unordered materials list with one line each on why the material matters, the rubrics become a one-sentence statement of the question the gate answers ("would you hand this plan to an implementer as-is" / "does the delivery honor the contract the spec set"), and the output prescription becomes report what materially bears on the decision, anchor it, end with an unambiguous verdict, and say plainly when the item is clean instead of inventing findings. Both templates now grant explicit licence to investigate, since the reviewer is an agent and not a reader. One gap closed along the way: the implementation-stage materials list now names roadmap/workflow.adoc, whose per-gate obligations (the user-facing-doc check and the retirement sweep) the old template never surfaced, so a reviewer had no route to two checks the workflow requires before approval. A Template design intent section pins the shape against re-accretion. The skill’s Procedure, Output rules, and Hard rules are untouched: template consistency across reviewers is itself a workflow property and was not up for relaxation. The sibling reviewer-prompt skill stays out of scope by the Spec-stage reviewer’s explicit answer to the item’s delegated question, since its "What to look for" taxonomy doubles as the project’s canonical review taxonomy. Templates went from 1126 to 942 words combined, with the surviving bulk being the mandated invariants. One amendment landed during the In Review window at the user’s request (11947dd): stripping the machinery had left the templates reading as pure gatekeeping, so both now open by stating that the review exists to help the item land as well as it can, and close by asking for improvement opportunities alongside blocking problems while keeping the two distinct in the verdict. A problem blocks; at the Spec gate an opportunity is the author’s call, at the Done gate an improvement the contract does not demand becomes a follow-up Backlog item rather than rework, and new scope routes to a fresh Backlog item at either gate. That is goal-level framing rather than a reinstated rubric, so it sits inside the design-intent paragraph rather than against it. Independent-session In Review → Done review, itself conducted through the new implementation-stage template; full reactor green under -Plocal-db, 13/13 modules.

  • R548 (c44ed95, javadoc scoping 51dfb78; Spec a32d87d`+be8484f, Ready `60e6dc5, In Review a74895c): Collapsed the manual’s two hand-maintained directive indexes into one and made the survivor build-enforced. reference/index.adoc reproduced the directives landing’s alphabetical and by-category columns inline and had drifted to 24 of 29 entries, missing @asFacet, @pivot, @routine, @scalarType and @tenantFanOut; since the manual landing routes its Reference quadrant at that page, the path manual → Reference → Directives ended on the stale list with no route to the complete one, which is how a reader concluded @pivot was undocumented. Deleted the duplicate rather than syncing it, leaving a single xref plus a descriptive sentence in the shape the same page already uses for Runtime API, Maven plugin, Editor support, Diagnostics and Deprecations. Generating the index from the SDL was considered and rejected: the alphabetical column is enumerable, but the deprecation and rejection annotations and the category grouping are editorial judgement, so the item guards the hand list instead of half-generating it. DirectiveDocCoverageTest grew a second method extending its seam from SDL→pages to SDL→pages→index: the alphabetical column must equal the page set exactly, and every page must be named somewhere under == By category. The columns are sliced on the two headings rather than regexed whole-file, since the columns overlap and a whole-file match would make the category assertion vacuous. Closing the guard exposed two gaps in the survivor it now rejects: @referenceFor was filed under no category and @record had lost its Mapping slot when @scalarType was added beside it. Membership is guarded; the editorial annotations beside an entry stay prose, and link validity remains `ManualXrefIntegrityTest’s (verified: a by-category entry naming a nonexistent page fails there, not here).

  • Discarded: Invert the command flow: the core produces method commands, the shell consumes them (core-produced-method-commands, R546): absorbed wholesale into R549 (facts-and-commands) three days after filing, before any implementation. The item’s own open Spec question was what shape MethodCommand should grow into beyond its four strings (coordinate, unit, type path, method name), and R549’s reframing answers "none": the model’s sealed hierarchies are the commands once each sits at its true grain, so a parallel command record is exactly the intermediary model that programme argues is unnecessary. Nothing in the item is lost. Its scope (flip the reentry family from shell-asks-core to core-tells-shell, oracle green, emitted output byte-identical) is R549’s slice 5. Its strongest justification, that CompileDependencyGraphBuilder.fromModel is a 731-line second derivation of the emit call graph whose duplication generates a recurring bug class (R455’s oracle blind spots, R459’s missing node, R462’s missing outgoing edges, all one shape: the graph is derived from coordinates while the emit contains methods no coordinate exposes), is R549’s slice 7, with the full argument and its caveats preserved as gap 7 of roadmap/audits/2026-07-26-fcis-command-layer-distance.md. Its abandon condition and 2026-07-26 baseline became R549’s. R545 (model-free-of-emit-vocabulary) survives independently as R549 slice 2’s invariant 3.

  • R542 (0a3c0c6; filed 646e779, duplicate-id collision resolved d77c03c, Backlog → Spec 7fd05fe, Spec → Ready 5e32f47, In Progress → In Review 85d2f52): CLAUDE.md is the one document every session reads in full and first, so a wrong fact there is believed without verification and acted on before any file is opened. Four factual errors, three misaimed pointers and one ineffective hook corrected, with a mechanical guard on each of the two drifts that had already recurred silently. The Java-17 floor is two modules for two different reasons, and only one of them was stated: graphitron-sakila-example compiles emitted sources at 17 to verify the generator’s output ceiling, while graphitron-jakarta-rest pins its default-compile execution to 17 because consumers put that hand-written jar on their classpath, making it a constraint on what a contributor may type there. The text now matches the three-category framing in development-principles.adoc. The module enumeration named eleven of twelve, omitting docs, while the same file’s Documentation site section correctly described docs as a Maven module; modules.adoc carried the identical off-by-one in prose and omitted the docs row. The test-tier pointer aimed at development-principles.adoc, whose own text defers tier names and the decision rubric to how-to/testing.adoc; it now aims at the rubric with the principles page kept as the why. Common commands documented no way to run less than everything, so the scoped inner-loop command and -Pquick are named, with the -am / -amd precondition moved into the surrounding prose so it stops contradicting the bare--pl rule three lines above. The publish skill is named as the default git path (the raw commands lose its dirty-tree stop, wip/draft/spike check, trunk-divergence pre-check and network retry) and the em-dash rule now says what it is: guidance binding newly written prose, no build gate, existing occurrences explicitly not a cleanup backlog. Two roadmap-tool tripwires bound to verify in the AdocMarkdownTableCheck shape close the drifts for good. TransientCitationCheck fails the build on an item id or non-permanent roadmap/ path in a declared prose document, which is where the rule that RoadmapReferenceGuardTest enforces across Java sources had no reach: markdown has no comment or string-literal regions, so the two files an agent reads first sat outside every scan and rotted five citations. ModuleEnumerationCheck requires every module the root pom declares to appear as a backticked identifier in both enumerating documents, checked in one direction only since a module identifier and an ordinary backticked word are indistinguishable in prose. Both treat a declared document that moved, and a root pom parsing to no modules, as failures rather than a smaller scan: the floor against a vacuous pass. The trunk-reminder hook emitted systemMessage, which is user-facing, so a nudge aimed at the session that had just pushed landed on the human to relay; it now emits hookSpecificOutput.additionalContext. Moving the payload exposed that the declarative if scoping was never filtering (a systemMessage nobody reads is indistinguishable from one that never fires), so the command body filters on the tool input itself, which also retires a latent bug where reading one line off jq missed a push on the second line of a compound command. Deliberately left open and filed as R547: the same citations live in nine pom.xml comment blocks and in the published docs/architecture/ pages, including two in the modules.adoc row this item edited. Coverage: 24 unit tests across the two checks, each asserting this repository clean from the test tier as well as at verify. Independent-session In Review → Done review re-derived every corrected fact against its file, exercised both guards end to end against a deliberately drifted tree, and ran the hook body against the four tool-input shapes plus a missing-command payload; full reactor green under -Plocal-db.

  • Discarded: Finish retiring pure-verdict GraphitronSchemaBuilderTest rows onto the classified corpus (classified-corpus-verdict-row-retirement, R539) (726cc71; filed 632c676, prose corrections 60b2933): filed on a misreading and discarded the same session, once the rest of the audit was read. R281’s verdict-row grind is finished: roadmap/audits/classification-test-dsl-inventory.md has all 35 whitelisted rows ticked plus a Slice 3 closeout section recording the endpoint (union net retired, output-field and type coverage moved to ClassifiedCorpus as single source of truth, long tail swept in), and VariantCoverageTest.everyOutputFieldAndTypeLeafIsDemonstratedByTheCorpus confirms it in code by reading coveredLeaves() alone. A re-derivation over the live enum found zero pure-verdict rows left: of 400 ClassificationCase constants the buckets are rejection/warning, slot-asserting, and input-side, all excluded from retirement by design, and the five apparent pure hits all read returnType().wrapper() asserted with isInstanceOf, which is a slot read, not a verdict. The misleading signal was a large surviving GraphitronSchemaBuilderTest (394 of its 400 rows are the excluded buckets) plus a staleness banner that flagged only the row list, which reads as "whitelist unusable, work still pending" rather than "work done". Corrected across five surfaces in 60b2933 (doc-only, no behaviour change): the inventory banner now leads with "closed and historical" and sends readers to the closeout before they draw conclusions about outstanding work; R25 (rebalance-test-pyramid) and R448 (routine-chain-residue) dropped their present-tense "is migrating" / "the grind" framings; ClassifiedCorpus.coveredLeaves()’s javadoc stopped describing the union net slice 3 retired (it claimed an enum row may retire once its leaf is covered there, which would license an unsafe deletion); and the `classified-corpus skill was reframed additive-first with its substrate de-rotted, having cited three symbols that no longer exist (LeafTupleAdapter, ProducerStep, Mapping), the pre-R316 two-axis (producer, mapping) vocabulary instead of (source, operation, target), the retired union net as its retirement safety net, a renderer caveat about absent fragment and input-type-closure support that has since landed, and the exhausted whitelist as step 1’s starting point, plus two contradictory guardrails about trusting VariantCoverageTest (resolved: it proves leaf coverage, not coordinate coverage). Still open and unowned by any item: the audit’s Excluded by rule note promises "a separate mechanism replaces these" for the rejection rows, the largest remaining block in the enum.

  • R526 (4ae2ce6 core, reviewer-pass javadoc altitude fix in the approval commit; filed 6c89e93, Backlog → Spec bb6c646, Spec → Ready 2218558, Ready → In Progress 665d9ec, In Progress → In Review c22bb0d): Six comment claims the R524 trimming sweep routed out because each needed a code-level decision, resolved per site, with two of the routed premises overturned by investigation rather than transcribed. GraphitronType.JooqTableRecordType now names both fqClassName populations (reflected table-record class vs the stand-in mints that assert only a projected-row source) and points at PojoResultType’s permit-identity split as the intended carrier, without blessing the null; `ReturnTypeRef.ResultReturnType states the copy contract (null exactly when the source component is) and TypeClassification.JooqTableRecord’s third restatement was aligned in the same edit. The garbled `InputType sentence is reconstructed against the code, not the spec’s draft: an input carrying @table always classifies as TableInputType because TypeBuilder.buildInputType short-circuits before the binding resolver runs, so the @table disjunct the spec proposed was false and the two real sources (the reflected method parameter the input flows into; the enclosing bound input’s accessor for a nested input) are named instead, with TypeClassification.PojoInput and PojoInputType’s own false "not specified in the directive" attribution corrected alongside. The `ErrorChannel emit partition became compile-checked: a new sealed ErrorChannel.RouterDispatched (permits PayloadClass, LocalContext) partitions the hierarchy against Mapped, WithErrorChannel.errorChannel() widened to Optional<? extends ErrorChannel> so each field variant declares its narrowest partition as its own record component, and the catchArm / asyncRouterCall seams plus ChannelCatchArmEmitter.emit took the narrowed types ; retiring all three "Additive window" runtime throws, the emitter’s unreachable LocalContext arm, and its sentinel parameter. Reachability analysis first established there was no live bug (Mapped is minted only by FieldBuilder.buildServiceField and routes exclusively through the wrap fork), so the narrowing pins a property rather than fixing a defect. RecordBindingResolver#fromAnyProducer, its reachable set, and the set’s population are deleted; the one live reachability read already derives the fact at the read site. Five dead buildSplitQueryDataFetcher / buildRecordBasedDataFetcher cites resolved three ways, with GraphitronSchemaValidator’s guard rationale truth-verified arm by arm against `buildBatchedDataFetcher rather than renamed, and one rationale made honest: BatchedLookupTableField now routes through the same guarded arms, so its exclusion is stated as an unmade validator decision instead of the stale "not audited". Claim 3 was deliberately not patched here ; a local fix would have added a differently-worded fifth statement of the empty-slot On.ColumnPairs state R527 was about to delete, so the site went onto R527’s inventory instead and R527 has since removed it. Follow-ons filed: R528 (carrier shape), R529 (@tableMethod sub-shape build-time rejection), R533 (single-source the null-source-guard predicate). Reviewer-pass delta: FieldBuilder.buildServiceField’s javadoc named four service variants where eight now use it; restated at altitude per "principles are stated at altitude" so the inventory cannot rot again, and R536 filed for the always-empty `ErrorChannel.Mapped slots on the polymorphic and table-interface variants that the count discrepancy exposed. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R417 (dbc1b87; Spec b381122, survey fix 7ed10f9, Ready 160bec6, In Progress 4665877, In Review f14ccc7): graphitron-sakila-example/README.md app section reconciled with the R399 jakarta-rest extraction. The "three files cover the runtime" framing and the dead GraphqlEngine/GraphqlResource/AppContext links are replaced by the one hand-written adapter, SakilaGraphitronApplication (linked; its gate-protected javadoc stays the single narrated copy) on the owned-connection path, with everything HTTP-shaped attributed to graphitron-jakarta-rest; the newExecutionInput() mention carries the placeholder-credentials caveat pointing at "Producing the claims payload", both README halves name their runtime path, the test-pattern section is restated as an invariant (HTTP tier under app/ with GraphQLOverHttpConformanceTest as the canonical exemplar, querydb/ in-process) instead of the rotted census, and the pom-shape sentence gains graphitron-jakarta-rest + Yasson. New enforcer ReadmeLinkIntegrityTest (@UnitTier markdown twin of ManualXrefIntegrityTest) fails the build on any dangling relative link in a reader-facing README.md, verified to bite on the pre-fix README’s three dead app/ links; modules.adoc now names this README as the one deliberately-maintained per-module README, with the guard as its enforcer. Ride-along: dropped the doubled graphitron-rewrite/graphitron-rewrite/ segment from the four live-doc GitHub URLs (docs/quick-start.adoc, docs/manual/tutorial/index.adoc). Routed out: R530, a manual how-to for the GraphitronApplication SPI adapter shape. Independent-session In Review → Done review re-verified the guard’s bite on the pre-fix README; full reactor green under -Plocal-db.

  • R335 (5016e6f; Spec f5de169, In Progress 38f38ad, In Review d856b77): The single classify-and-emit walk (R317) now classifies the whole reachable surface, not just the output composites: SchemaReachability’s child function descends the input edges (field argument types, input-object field types; `outputTargets renamed fieldTargets), survivor directive definitions seed their argument types (so federation__FieldSet on @key classifies), ClassifyingVisitor gained input/scalar/enum leaf arms, and the pre-walk leaf sweep in TypeBuilder.prepareForWalk was deleted. Leaf classification is now reachability-pruned exactly like output composites: a declared-but-unreached input/enum/scalar is absent from types(), including a SortDirection retained by the all-declared retainedSupportTypes() scan but reachable from no coordinate. The spec’s enabling fact (registry-read-free field classification) held lexically for FieldBuilder only; the transitive leaf reads were re-routed rather than worked around: keyed reads go through the new BuildContext.lookAheadVerdict (delegating to the memoized TypeBuilder.lookAheadVerdict), the scalar axis became the BuildContext.scalarVerdicts fixed point built beside the node/table/error indices, and classifyAndRegister now registers lookAheadVerdict’s verdict so a reachable multi-producer-rejected input re-registers the exact seeded `UnclassifiedType (equals-idempotent, typed RecordBindingMultiProducer payload preserved). Pinned by SchemaReachabilityTest.walkClassifiesLeavesAndPrunesUnreachedOnes, SingleWalkClassificationOrderTest.noInputTypeIsRegisteredBeforeItsDiscoveringFieldIsVisited, and R96RecordBindingPipelineTest.multiProducerInput_reachableThroughTheWalk_keepsTypedRejection. R531 filed for the classify-time registry-read meta-test. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R525 (eaae5ff, filed ec86c95; collapsed Backlog → In Review flow): TenantFanOutExecutionTest.claimedButUnmappedTenant_failsTheRequestBeforeAnySql failed roughly one run in ten with no product defect: its redaction guard !e.getMessage().contains("99") collided with the random hex correlation id embedded in the redacted message ("An error occurred. Reference: <uuid>."), since a 32-hex-digit UUID contains the substring 99 in about one draw out of ten. The predicate now matches the full redacted template (An error occurred\. Reference: [0-9a-f-]{36}\., the shape GraphQLOverHttpConformanceTest.REFERENCE_MESSAGE already asserts), which proves the reference is present and that nothing else, tenant key included, rides in the message; a comment above the assertion records why a negative contains check is unsound against a correlation-id-bearing message. Sibling sweep found no other unsound instance (downedTenant’s `doesNotContain("tenant 2 is down") cannot occur in hex; GraphQLQueryTest’s `doesNotContain(wrongTypeId) probe is URL-safe Base64 starting with an uppercase letter, unrepresentable in a lowercase hex UUID). Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R494 (49e4b84, In Review 23d58e9; assessment 1bae5fe, Spec 6da07b0, Ready cdab0c8, In Progress 28e27e3): The hand-maintained SchemaDirectiveRegistry.GENERATOR_ONLY_DIRECTIVES survivor-filter set had drifted three names behind the declared directives (routine, asFacet, pivot), so their definitions and applications leaked into every emitted schema surface (the sakila schema.graphqls files and the programmatic GraphitronSchema/per-type classes), exposing build-time database internals such as routine names and column mappings to any introspecting consumer. The existing pipeline sweep SchemaSdlEmissionTest.emittedSdlCarriesNoGraphitronInternalSurface was blind to exactly this drift class because its oracle was isSurvivor itself. Fix derives the set instead of extending it: new DeclaredDirectives in rewrite.schema exposes the directive-definition names parsed from the bundled directives.graphqls, sharing a single parse with DirectiveSupportTypes (same resource, same derive-not-hand-maintain precedent), and GENERATOR_ONLY_DIRECTIVES now reads that set, so a directive is generator-only iff declared in directives.graphqls and the drift class is unrepresentable; the registry javadoc drops the severed "kept in sync with BuildContext DIR_*`" contract (the `DIR_* constants stay hand-maintained deliberately, as classifier lookup keys). The spot-check unit test became a 32-name containsExactlyInAnyOrder pinning test (mirroring DirectiveSupportTypesTest), which also guards against graphql-java ever injecting built-in directive definitions into the parsed registry; the emission sweep, now non-circular, extended from three to all five plugin executions (adds multitenant, multischemamutation). Independent-session In Review → Done review verified the regenerated sakila outputs carry no routine/asFacet/pivot definitions or applications (only survivor auth/oneOf applications remain programmatically); full reactor green under -Plocal-db. Surfaced by the R483 javadoc drift audit.

  • R97 (Phase 2 85d8be0, Phase 2b 30fde4b; Spec 8682ef0/47f6765, Ready 0993d11, In Progress 117bd9a, In Review 4f7326a): Consumer-derived input-table resolution, the slice of the @table-on-input deprecation that shipped. Phase 2 made the last TableInputType-gated call site (arg-level @lookupKey) consumer-derived: a plain (non-@table) input carrying @lookupKey re-derives its fields against the consuming field’s return table through TypeBuilder.resolveInputFields and builds the same TableInputArg the @table bridge produced (FieldBuilder.classifyPlainLookupKeyArg), so the composite-key lookup shape (FilmActorKey) no longer requires @table on the input; a field that does not resolve to a lookup column on the consumer’s table is a classify-time rejection naming that table, never a silent no-op. EnumMappingResolver.buildLookupBindings widened from a TableInputType to a resolved List<InputField> (both callers reach it fully resolved, so the SDL re-walk and its dead null arm went). With that arm consumer-derived and the mutation verbs field-relative, TypeBuilder.buildInputType is now directive-driven only (explicit @table → the deprecated bridge, everything else plain): the findReturnTablesForInput auto-promotion aggregate and the isUsedWithOverrideCondition routing gate are both deleted, so an input reused across tables resolves per-consumer instead of silently demoting. The LSP surfaces the inference: TypeClassification.PojoInput gained resolvedTables, read off each consuming field’s classified target (never a re-read @table), and the hover lists them per-consumer. Phase 2b gave UPDATE a field-relative write target by extending R515’s precedence ladder (return-derived table > @mutation(table:) > the deprecated input @table bridge): INSERT and UPDATE now share one resolver (FieldBuilder.resolveReturnCapableWriteTarget), UPDATE joined RETURN_DERIVED_TABLE_VERBS and TABLE_ARG_SUPPORTED_VERBS, rawArgUpdateRejection and the "@mutation fields only accept @table input" refusal are gone, and the unsupported-verb guard narrowed to {UPSERT}; the R332 deprecation warning gained a UPDATE-consumed clause. Coverage: plain-@lookupKey resolve/reject pipeline cases, per-consumer PlainFilter rewrites, four re-pinned auto-promotion cases, resolved-table hover cases, UPDATE carrier-equality cases (direct/single/bulk) with cross-check and no-write-target rejections, and a updateFilmReturnDerived sakila execution round-trip with @table dropped from the input; carrier equality, never code-string assertions. The remaining phases are their own items: R518 (argMapping grouping fan-out), R519 (remove-table-on-input-directive) ; now the home for the general @table-on-input directive removal ; and R520 (housekeeping). The shared axis rationale (fact-model framing, redundancy argument, what @table-on-input still drives) lives permanently in roadmap/concepts/consumer-derived-input-tables.html, referenced by R519, so this item’s deleted spec file leaves no dangling reference. Independent-session In Review → Done review; full reactor green under -Plocal-db (all 13 modules including the sakila PostgreSQL execution tier). Builds on R457/R514/R515.

  • R515 (bacfb99; review fixes 639e277; Spec 63f6efa, Ready 0faa819, In Progress 89b3d8f, In Review 3f109f8): INSERT derives its write target from the return side, so an INSERT input no longer needs @table. The write target now resolves by a precedence lattice symmetric with DELETE’s (R457) but with the return-derived rung DELETE structurally cannot have: rung 1 (preferred) is the return’s own @table ; a direct @table return, or a carrier payload’s single @table-element data field, both already carried by the classified model (TableBoundReturnType.table() / the structural payload scan’s DmlElementKind.Table); rung 2 is @mutation(table:) on the field, for the encoded-ID / scalar return whose return names no table (INSERT joins the single-sourced TABLE_ARG_SUPPORTED_VERBS, so the unsupported-verb guard narrows to {UPDATE, UPSERT} and the change flows to mvn graphitron:validate, the LSP, and the binding grounder automatically); rung 3 is the deprecated input @table bridge. It is a must-agree lattice: where rung 1 is present a present rung 2 or rung 3 must name the same table (rung-1-vs-rung-3 reuses the pre-existing requireDmlDataTableMatchesInputTable wording byte-identically; rung-1-vs-rung-2 rejects because the RETURNING projection reads from the write target), rung 2 silently outranks the rung-3 bridge (byte-matching DELETE), and a no-rung INSERT rejects with a message leading with the return-derived fix. The write-target fact stays single-produced: R514’s phase-portable MutationInputResolver.resolveDmlWriteTableRef gains the return-derived rung and both verb gates, so the classifier (FieldBuilder.resolveInsertWriteTarget, dispatched to from the new classifyInsertTableField / classifyInsertPayloadField mirroring the UPDATE/DELETE walker dispatch) and the binding grounder (RecordBindingResolver.groundDmlMutationField) cannot diverge; a pipeline-tier enforcer pins grounded-table == classified-write-target across all three rungs. INSERT is intercepted before the now-retired MutationInputResolver.resolveInput (its INSERT admission set ; admitMutationInputFields, rejectPlainColumnCollision, rejectInputFieldDirectives ; hoisted to shared statics and run over one resolved InputField list regardless of path; the field-derived path re-derives fields through TypeBuilder.resolveInputFields and mirrors GraphitronSchemaValidator.collectInputFieldRejections); the UPSERT deferral moves to the dispatch. The R332 @table-on-input deprecation warning is now actionable for INSERT: the encodedWriteTargetInputTypes carve-out is retired (its stated retirement condition met) and the warning fires on those inputs, naming the return-derived fix and @mutation(table:). Coverage: byte-identical carrier parity (payload single/bulk, direct @table return) between the @table-on-input and return-derived forms, the three rung-mismatch and outranking cases, the no-rung message ordering, admission-set parity across both paths (composite-@nodeId, plain-column collision, @lookupKey/@condition), the grounding-divergence enforcer, a sakila bulk INSERT execution round-trip with @table dropped, and the deprecation-warning flips; mutation.adoc / table.adoc / deprecations.adoc / code-generation-triggers.adoc updated. The In Review → Done review (a different session from the implementer) also landed two fixes on the revision (so this session is not itself the sign-off on those edits): an unresolvable @mutation(table:) naming under a present return-derived rung was silently ignored (rung 1 short-circuits the helper before rung 2’s name is validated) and now rejects like any unknown table, and prose referencing the retired resolveInput (a dangling {@link} in the grounder javadoc, plus EnumMappingResolver / DmlKind claims that INSERT still routes through it) was refreshed. Full reactor green under -Plocal-db.

  • Discarded: Bounded parallel execution substrate for tenant fan-out (tenant-fanout-parallel-execution, R510): absorbed wholesale into R46 (service-multi-tenant-fanout) as its "Execution substrate: bounded scatter" section and leading slices, once it was clear R46 is the scatter helper’s first and likely only caller; the principles consult on the R510 Spec had already flagged that shipping the substrate with its first execute-tier caller was the fallback if direct concurrency proofs proved impractical, and the R45 precedent (routing surface as an early slice ahead of its fetcher emission) covers the slice shape. Everything worth keeping carried over: the policy-neutral per-tenant Outcome scatter contract, executor/cap/timeout on the GraphitronRuntime constructor, the TenantConnections thread-safety rework with the straggler and re-entrancy invariants, and the considered-and-deferred full-async-fetchers record with its evidence trigger.

  • R511 (d9c4b95; Spec 2e4fe78, Ready f44fab4, In Progress 3d61fce, In Review f0516bd): A @splitQuery @service child whose key wrap is SourceKey.Wrap.TableRecord (the ergonomic Set<XRecord> service signature) rebuilt the parent’s DataLoader key by reading reserved __src_<col>__ aliases off env.getSource(). Those aliases exist only on an SQL-projected parent row (<Type>.$fields adds them under the reserved-alias scheme that fixed the multiset-alias collision); a @table type handed back raw by a @service (selectFrom(X)Result<XRecord>, never through $fields) carries only the real columns, so every such child threw IllegalArgumentException: Field "__src_..." is not contained in row type at runtime, defeating a combination the manual already documented as supported. Fix makes GeneratorUtils.buildKeyExtraction’s `TableRecord arm runtime-adaptive on the source’s own type: a typed parent (source instanceof XRecord) is copied column by column by jOOQ field-identity (never a by-name into(...) map, so the multiset-alias collision cannot re-enter), and a generic SQL-projected row is rebuilt from the reserved aliases as before. Both arms mint a fresh record over the same allColumns() enumeration, so the produced key is structurally identical whichever path produced the parent and the live parent is never aliased as the DataLoader key. The fork follows the sanctioned "runtime shape of env.getSource() forks the read" pattern (dispatch-axes.adoc); it is emitted unconditionally rather than gated on a producer-reachability model fact (declined in the spec: a missed path in that walk would reproduce this very bug with the gate asserting it cannot happen; the unconditional fork degrades to one dead-but-correct arm instead of a crash). Producer side untouched: <Type>.$fields, RequiredProjection.reservedFullRow, and ParentProjectionContainmentCheck are byte-identical to pre-fix output. The discriminator’s basis (a $fields SELECT materialises a generic Record, never the typed subclass) is pinned by ServiceParentTableRecordKeyExtractionTest (graphitron-sakila-example internal tier), named in prose from the read-site javadoc rather than by {@link} because the pinning test lives in a downstream module off this module’s javadoc classpath. Coverage: the execution-tier spine filmsByService { titleTitlecase } (service-returned FilmRecord parent; pre-fix threw on the missing __src_ aliases) with the SQL-parent kind still pinned by the existing colliding-multiset fixture, the two-sided discriminator pin, and a ServiceProjectionPipelineTest shape predicate asserting the generated fetcher body carries both read families (shape assertion, not a code-string body pin). handle-services.adoc tightened to state the two-parent-kind mechanism plainly. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R126 (26607a1/51559d3/e472187, In Review ab17c29, Spec 7658c81/994e7c3/6cf2c9b): Scrub the deleted classification vocabulary from prose across every surface. The retired SourceKey.Reader four-arm family (ColumnRead/SourceRowsCall/AccessorCall/ProducedRecordRead), the BatchKey.* permits (AccessorKeyedSingle/AccessorKeyedMany/LifterPathKeyed), SourceKey.Cardinality (bare Cardinality.ONE/MANY), and the Mapped{Row,Record,TableRecord}Keyed service-source shapes no longer exist as types, but still appeared in generator comments/javadoc, one main-source invariant message, test case-descriptions, sakila-service/sakila-example fixture prose and SDL comments, and the author-facing user manual, keeping rot alive in code search and misdescribing what the classifier produces today. Each dead arm maps one-to-one onto its live successor: Reader.AccessorCall/BatchKey.AccessorKeyed*KeyLift.Accessor(AccessorRef, Arity); LifterPathKeyed/SourceRowsCallKeyLift.Lifter(LifterRef); ColumnReadKeyLift.FkColumns(); ProducedRecordReadKeyLift.ProducedRecords(Arity); Cardinality.*Arity.*; Mapped*KeyedSourcesShape(SourceKey.Wrap.*, LoaderRegistration.Container). The mis-shaped SourceKey(Wrap.Row, parent.PK, ColumnRead, Cardinality.ONE) constructor prose (which named components SourceKey never carried) is rewritten as a KeyLift.FkColumns lift with a residue SourceKey over the parent PK. Identifier renames taken in full per the Spec → Ready sign-off (dead type names embedded in test-tier and fixture identifiers keep the vocabulary in code search): AccessorDerivedBatchKeyTestAccessorDerivedSourceTest, GraphQLQueryTest.inventoryById_filmCardData_firesAccessorKeyedSingleLift......firesAccessorLift... (with its computed-fields.adoc citation), four ServiceCatalogTest classifiedAsMapped*Keyed methods → classifiedAsWrap*, InterfaceFieldValidationTest.rejects_listArm_onAccessorKeyedMany......onAccessorMany..., and the SDL-cited fixture methods TestFilmService.getRankMappedRecordKeyed / TestServiceStub.childServiceMappedRecordKeyedWrongScalarValue, each with its lockstep call sites. The scrub also corrected stale FACTUAL capability claims discovered on the same subsystem (all doc-only, verified against current code): the polymorphic parent-key path is not "follow-up work" (FieldBuilder.derivePolymorphicHubSource produces a KeyLift.Accessor key for class-backed parents, consumed by MultiTablePolymorphicEmitter’s `KeyLift.Accessor branch and pinned by KeyLift.checkResidueAgreement); classifyParticipantRoute does construct ParticipantCorrelation.JoinedCorrelation for multi-hop/condition routes (not a deferred rejection), emittable via branchBridgingJoins/singleBranchCorrelationWhere; ChildField.Transport has three live arms including WrapperArm; TableTargetField permits six variants, not eight; the error-channel carrier classifier shipped (FieldBuilder.resolveErrorChannel); a single-table discriminated @service interface routes to the sibling {Mutation,Query}ServiceTableInterfaceField leaf rather than being deferred; and two stale symbol citations (deriveBatchKeyFromTypedAccessor, TypeFetcherGenerator.buildScatterByIdxHelper) repointed to the live methods. The retirement-provenance carve-outs (mentions explicitly framing the vocabulary as retired, in model/Arity.java, Target.java, KeyLift.java, KeyLiftTest.java, SourceKeyTest.java) stay as documentation of the retirement. Pure doc/identifier scrub: one main-source string edit (the ParentCorrelation invariant message), no production-model, generated-output, or test-behaviour change; the existing tiers remain their own enforcer. Acceptance is the Spec’s sweep grep leaving only the enumerated carve-outs. Sibling item R504 owns the ChildField leaf-taxonomy vocabulary (Single/Record/Split*TableField), deliberately out of scope and not bled into. Independent-session In Review → Done review; full reactor green under -Plocal-db including RoadmapReferenceGuardTest and the javadoc {@link}-reference gate.

  • R500 (6cc947f, In Review 55818e7, Spec 9d1a788): Aliased duplicate reference selections (a: ref { x } b: ref { y }) minted duplicate SQL aliases, because the $fields selection switch projected each inline reference / computed field by its schema field name rather than its runtime result key: two result-key buckets sharing a field name fired the same arm and emitted two DSL.multiset(...).as("<fieldName>") terms under one alias (a loud duplicate-alias jOOQ error), and the source-only reads could not tell two aliases apart. Fix aliases the four families (list and single TableField, LookupTableField, ComputedField, Direct ColumnReferenceField) by "__rk_" + entry.getKey() on the write side and reads them back by "__rk_" + env.getField().getResultKey() on the read side, so each alias resolves independently; divergent sub-selections and divergent arguments across aliases both come out correct (each result-key bucket emits its own arm). The reserved prefix (GeneratorUtils.RESERVED_RK_ALIAS_PREFIX, single-homed for both sides beside the __src_ precedent) moves minted aliases out of the client-reachable namespace, so a client alias can never shadow a base column, a __src_ full-row alias, or __discriminator__ (an adversarial __rk_foo mints __rk___rk_foo, still distinct). The four families' reads move from source-only LightFetcher bindings to env-dependent bindings; scalar ColumnField / CompositeColumnField arms stay as-is (alias-independent typed-constant reads). Membership single-homed as the recommended design fork: a methodless marker ResultKeyAliasedField on the four families plus a build-time guard on each consuming side’s fall-through (TypeClassGenerator.emitSelectionSwitch’s `default arm and FetcherEmitter’s method-backed fall-through both throw when a marked variant reaches them unhandled), so a future alias-projecting variant that forgets either side is a loud generation-time failure. Two deviations from the Spec’s "pure emit-layer, no validator change", both forced by the reserved-prefix design and confirmed sound on review: (a) `GraphitronSchemaValidator.validateAliasKeyColumnCollisions (and its parentProjectionAlias cluster) was retired because a __rk_-prefixed alias can never equal a base column name, so the shadow it guarded is structurally impossible and leaving it would false-positive-reject now-legal schemas; its test was repurposed to ReferenceProjectionAliasNamespaceTest; (b) the single-table @discriminate base-slice (TypeFetcherGenerator.buildInterfaceFieldsList) hand-projects inherited references and shares the one result-key-aware read, so it now projects each inherited reference per selected result-key bucket, agreeing with the read on both the unaliased and aliased-duplicate paths (left unchanged it would have regressed even the unaliased allSubjects/allParties case). Coverage: unit/pipeline signature assertions (env-dependent method references, no code-string body assertions), ReferenceProjectionAliasNamespaceTest, and five execution-tier enforcers in GraphQLQueryTest (mixed aliased+unaliased scalar/computed, LookupTableField divergent args + sub-selections, list TableField duplicate, single-cardinality nested reference, adversarial __rk_-prefixed client alias). Independent-session In Review → Done review; full reactor green under -Plocal-db (all five new execution tests ran, none skipped). Two test-plan scenarios named but not shipped, both low-risk given the shared $fields/reserved-prefix mechanism and left as possible follow-up: aliased duplicates under an @asConnection query (edges.node / nodes), and a base-column-named client alias as an execution variant (its validate-time equivalent shipped in the pipeline namespace test). Sibling to R499 (within-bucket occurrence merge, orthogonal axis); surfaced during the R499 Spec trace.

  • R499 (0a399f5, In Progress 93e5d16, Spec f6d4d9e/1bb4ad6): A Relay connection’s <Node>.$fields projection built the SELECT from only the first occurrence of each shared result key, so selecting the same reference field under both edges { node { ... } } and nodes { ... } with divergent sub-selections dropped the diverging side’s columns and the mapper failed per row with a jOOQ "not contained in row type" error (silent null on that side). graphql-java’s getFieldsGroupedByResultKey() flattens the whole subtree, so the two paths collapse into one List<SelectedField> bucket per key; the loop bound sf = entry.getValue().get(0) and recursed over only that occurrence’s selection set. Fix moves the union onto the selection side: a new frozen-scaffold SelectionOccurrences (SelectionOccurrencesClassGenerator, registered in UtilSingleton.ALL) carries mergeByResultKey (concatenate every occurrence’s sub-selection into one insertion-ordered grouped map, so each arm still emits exactly one DSL.multiset(...).as(...) SELECT term per key rather than one per occurrence, which would mint duplicate SQL aliases), plus two consistency guards. Each type class now exposes two public $fields entries (DataFetchingFieldSelectionSet and List<SelectedField>) delegating to one private $fieldsGrouped switch loop; the inline TableField/LookupTableField arms descend via $fields(entry.getValue(), ...) and NestingField recurses over the merged map, so a sub-field requested under either path lands in the SELECT and each reader ignores columns it did not ask for. Divergence the single-name switch cannot represent fails loud as a GraphitronClientException (surfaced raw past ErrorRouter, not redacted): the name guard canonical(key, occurrences) is universal (runs per bucket before every dispatch, since occurrences disagreeing on getName() are two fields aliased to one key), the argument guard requireConsistentArguments is arm-scoped (emitted only where the arm consumes sf for runtime state, driven by the FromSelectedField predicate InlineTableFieldEmitter.readsSelectedFieldArguments; unconditional in the lookup arm whose input-rows helper always reads @lookupKey). Pure emit-layer fix: no classification, model, or validator change, one blanket frozen-scaffold compile-graph edge per type class. Coverage: SelectionOccurrencesClassEmitTest (scaffold signatures), OccurrenceArgumentGuardPipelineTest (guard emission tracks the predicate; lookup arm unconditional), and execution-tier ConnectionSharedResultKeyProjectionTest (10 tests: all four divergence directions on Store.customers, one level deep, a polymorphic searchConnection, argument divergence/agreement on customersFirstN, name divergence on aliased summary/info NestingFields, and a non-connection control). Out of scope and filed as R500 (aliased-duplicate result keys colliding on the SQL .as(...) alias); the whole-subtree flattening substrate stays as-is. Distinct from R481, which shares the "not contained in row type" symptom on a different path. Independent-session In Review → Done review; full reactor green under -Plocal-db (scaffold 5, pipeline 3, execution 10). Surfaced during runtime testing of the opptak subgraph against 10.0.0-RC27; spawns R500.

  • R484 (ae11947, In Review 608b793, Spec → Ready c1c0e5f): Purge transient roadmap references (R<n> / roadmap/<slug>) from string literals across the in-scope modules, and drop the Rejection.Deferred.planSlug field that rendered one. The structural centerpiece: planSlug is removed from the model, the deferred(...) factories collapse to deferred(summary) / deferred(summary, class), and Deferred.message() becomes a pure function of summary, so a deferred rejection can no longer compose a see roadmap/<slug>.md suffix onto the author-facing validator log or the runtime stub’s UnsupportedOperationException text. The field was unconsumed drift: nothing navigated it (the claimed LSP fix-it that read the slug never existed), its only two render consumers (the validator log surface and the validator-unreachable TypeFetcherGenerator stub) lose it cleanly, and the FieldBuilder DML-pair slug threading and TypeFetcherGenerator.deferredFor slug arg are dropped with it. Three string-literal habitats swept, each restating the fact or naming the live mechanism in prose rather than blind-deleting: author-facing rejection message literals (habitat A), internal invariant-throw messages (habitat B, e.g. "intercepted by their walker classifiers" replacing an R246 / R258 / R266 citation), and documentation text emitted into generated output (habitat C); plus the two @reference-path channel-bypass deferrals and the three synthesis-shim LOG.warn sites. Enforcement is one guard over two lexical habitats: RoadmapReferenceScanner gains a string-literal projection mirroring its comment projection (the same single lexer, appending in the string/char/text-block states instead of the comment states), and RoadmapReferenceGuardTest adds a pass over generator main-source string literals with its own scanned-file floor against a vacuous pass. The main-source scope is deliberate: habitats A/B/C all render to a consumer surface, whereas a test’s @DisplayName or assertion description citing an item as provenance renders to none, so the test tree is out of the string projection’s scope (the comment scan still covers test sources); CLAUDE.md and the scanner javadoc are updated to state this. Docs reconciled: typed-rejection.adoc drops the false LSP-fix-it claim, the planSlug field, and the non-existent StubKey.EmitBlock arm (from both the prose and the mermaid class diagram); four user-manual pages no longer promise a roadmap-path suffix. Test reworks assert on summary / stubKey rather than a slug, and RejectionRenderingTest pins message() both equal to summary and doesNotContain("roadmap/"). Subsumes and discards Backlog stubs R491 (habitat C + guard extension) and R493 (generated-output javadoc). Independent-session In Review → Done review (main-source scope confirmed faithful to the spec’s habitat set and FP-triage premise; guard, scanner, and rendering tests green, graphitron module compiles all call sites); full reactor green under -Plocal-db. Builds on R482 (comment/javadoc guard); subsumes R491/R493.

  • R488 (ab58f63, In Review 1432ff1, Spec → Ready 3b6ba97): Concept explainer pages declare the roadmap item(s) they back and cross-link them both ways from that one declaration. The page contract gains a second machine attribute on the <h1>, data-concept-items="R<n>[, R<m>...]", alongside data-concept-title: a comma-separated R<n> list (whitespace trimmed) naming the concept’s anchors. Three enforcers fire at build time naming the file, the same discipline as the title contract: well-formedness (each token R[1-9][0-9]*, required and non-blank), an allocated-id bound (n < changelog next-id, so a typo like R999 fails while a shipped item whose file was deleted on Done and a discarded item both pass, keeping shipped items legal anchors), and a kicker enforcer (each declared id must appear in the header kicker line, which now reads Concept explainer · R<n> · theme: <theme>, so the visible restatement cannot silently drift from the attribute). ConceptPages.readTitles (slug → title) is replaced by readPages (slug → ConceptPage carrying title + backing ids), taking the roadmap dir so it can read changelog.md’s `next-id: for the bound; generate, verify, and render-adoc staging all route through it, so every contract is enforced wherever pages are read. The live-vs-shipped question is resolved once in a new ConceptIndex: a declared id becomes a Live(id, slug) or Shipped(id) ItemAnchor (sealed, each variant carrying exactly its fields), plus a reverse index from item id to slug-sorted explainer slugs; render sites switch exhaustively over the resolved outcome and never re-derive liveness, mirroring the mapHref one-place discipline. Both directions derive from the declaration: item side, an explainer link follows the plan link in the README Active table and status board and a parenthesized link on Backlog/Deferred lines, one per backing page in slug order, only for live anchors; concept side, the Concept explainers listings annotate each page (backs R<n>), linked to the item plan when live and plain text when shipped. The one shipped page (per-participant-child-join-paths.html) is refit with data-concept-items="R458" and the kicker update, proving the Shipped arm live (below next-id, no listing link, plain-text annotation); the Live arm is covered by tests. The explainer skill contract documents the attribute, the kicker format, the generate-time derivation, and the refresh-time preservation rule (the attribute is authored, not a derived region). Coverage: ConceptPagesTest extended to 35 (items parsing incl. whitespace/missing/blank/malformed; allocated-id bound incl. the n == next-id boundary and the shipped-but-below case; kicker enforcer; readPages shape and absent-directory empty; ConceptIndex Live/Shipped resolution and slug-sorted reverse index; README and status-board rendering of both link directions incl. two-pages-one-item and the drift property; stage enforces the contract on every staged page). Out of scope and unchanged: backfilling beyond the one page, the tool-injected backlinks refinement (a distinct concept-to-item relation), and the by-theme views. Independent-session In Review → Done review; roadmap-tool green (122 tests, ConceptPagesTest 35/35) and README regenerates with no drift.

  • R483 (per-slice 2f2dcb8/a40a2e4/da2a248/5fe681d/e7a4a25/434dc90/44a9931/557f332/2f55024/19f4481, outcome + follow-ons 2d4b6c2, In Review d4ff640): Audit javadoc for drift against the current design and implementation across every in-scope module, correcting what had drifted and leaving each module more mechanically pinned than it was found. Ran as a 34-batch reader-plus-verify fan-out over all 517 main-source files (the module rows sub-batched at ~18 files so each was read by exactly one reader; the 7,648-line FieldBuilder was drift-marker-grepped rather than full-read), landing 89 net edits (90 applied, 1 reverted by the adversarial verify stage, which restored a live GeneratedSourcesLintTest.FORBIDDEN_IMPORTS-backed rationale a reader wrongly took for dead narration). The unit of work was not "rewrite prose to match code" but convert an unpinned claim into a form something mechanical breaks on: dead design narration deleted, live-symbol claims relinked to {@link} (now build-enforced by R492’s check-link-references gate), and load-bearing claims that could not be cleanly deleted or relinked routed to follow-ons rather than rewritten into fresh confident prose (the central hazard). The R492-seeded findings were resolved against real current symbols: ChildField.SplitLookupTableField repointed to the live BatchedLookupTableField, the retired BatchKey classification vocabulary corrected to the current KeyLift.* / SourceKey.Wrap.* names, and the stale legacy-GraphQLGenerator clause deleted. Six Backlog follow-ons were filed for still-unpinned load-bearing claims: R493 (roadmap ids in generated-output javadoc), R494 (SchemaDirectiveRegistry.GENERATOR_ONLY_DIRECTIVES out of sync with BuildContext DIR_ROUTINE/DIR_AS_FACET, a possible correctness bug), R495 (InputRecordGenerator service-audit javadoc contradiction), R496 (MappingsConstantNameDedup two-vs-three ErrorChannel arms), R497 (FederationSpec.URL caller census drift), R498 (RAG dev-warm hint pointing at a nonexistent doc section). All 12 ledger rows recorded a pass (10 with SHAs, graphitron-jakarta-rest and graphitron-sakila-example no-drift). Independent-session In Review → Done review (edits confirmed comment/javadoc-only, seeded findings and three spot-checked follow-ons verified against current code, every relink resolved); full reactor green under mvn install -Plocal-db with the R492 reference gate and RoadmapReferenceGuardTest active. Sequenced after R482 (de-noised surface); depends on R492 for relink to count as a real pin; spawns R493-R498.

  • R492 (adfcb5a, In Review e0baef1; Spec c5d284e/15ce2a0, Ready 78b9933): Add a {@link}/{@see}-reference-validity gate to the routine build, so a dangling doc link fails verify instead of compiling clean and shipping. javac ignores javadoc and the only prior resolver was the release profile’s javadoc jar, so a rename silently orphaned every {@link} naming the old symbol; in a text-editing multi-agent workflow the IDE-refactor tracking {@link} used to carry was near-zero protection. The gate binds the maven-javadoc-plugin javadoc goal (its own resolver, not a hand-rolled scanner that would have to reimplement import/nested/inherited/classpath resolution) with <doclint>reference</doclint> (only the reference group, not the missing-@param firehose) to verify in the main <build> — deliberately not an activeByDefault profile, which Maven silently deactivates whenever -Plocal-db is named; the fast-local escape reuses the existing maven.javadoc.skip lever that -Pquick already sets, and CI’s single mvn verify -Plocal-db picks it up for free. Turning it green meant driving the 41 pre-existing broken references to zero (measured graphitron 30, graphitron-sakila-service 10, graphitron-lsp 1): resolvable links were qualified or FQN-labelled and kept as checked {@link} (nested variants, cross-package public symbols, two cheap repoints OutputField#source(Arrival) and Table#getReferences()), while genuinely unresolvable targets — generated-runtime symbols, package-private cross-package, non-dependency cross-module, and dead symbols — were converted to {@code} (not the prohibited downgrade, which is only rewriting a resolvable link). Retired-vocabulary drift (BatchKey arms, ChildField.SplitLookupTableField, the legacy GraphQLGenerator clause) was flagged into R483’s seeded-findings rather than guessed at. Anti-vacuous-pass guard: JavadocReferenceGateTest pins that the gate is bound in the main build (not a profile), that no covered module opts out with maven.javadoc.skip, and that covered modules have hand-authored sources; the sourceless/generated-only modules are structurally exempt. development-principles.adoc had its now-false "`{@link}: no audit infrastructure" grade rewritten to name the gate (still under the `DocSizeBudgetTest word budget) and CLAUDE.md documents the -Pquick skip. Independent-session In Review → Done review (gate verified live per covered module, a planted dangling {@link} confirmed to fail the build, no prohibited {@code} downgrades on spot-check across every category); full reactor green under mvn install -Plocal-db (unpiped exit 0; one transient BgeEmbedderOnnxTest similarity flake under parallel load, unrelated, passed on rerun). Prerequisite for R483’s relink tool to count as a real pin; sibling to R482.

  • R314 (d1f13a2 site-level fact + command registry + bidirectional oracle, 7137d1e/4abde9e slices 2a/2b, 4e04345 slice 3, 11122a4 slice 4, 1158c14 slice 5, 88ddf90 docs sweep + In Review; plan 6590535, architect-consulted): Dissolve the re-fetch (reentry) leaf fields ; the reentry family becomes the first emit family driven by the R333 model instead of leaf identity. Slice 1 landed the site-level fact OutputField.emitsKeyedReQuery() next to the value-level requiresReFetch() (false exactly on the root @service passthrough, whose re-projection is realized by downstream $fields), the main-source methodgraph command/name registry (MethodCommand + MethodCommandRegistry, the name authority: emitters obtain the declaration name from the commit, whose gate reads the site-level fact, so a parallel register-beside-the-formula census shape is not constructible) surfaced on GenerationResult.methodCommands(), and thread I’s level-2 bidirectional closure oracle (ReentryCommandClosureTest: model → command, command → emit, exactly-one, plus non-vacuity witnesses and the root-passthrough / table-sourced-split boundary pins). Slice 2a merged buildSplitQueryDataFetcher + buildRecordBasedDataFetcher into the one source-shape-gated buildBatchedDataFetcher (the stored SourceShape gates exactly the two facts it owns, key lift and Outcome/LocalContext prelude; byte-identical sakila output). Slice 2b landed TableExpr.MethodCall as its javadoc reserved and dissolved RecordTableMethodField onto the record-sourced BatchedTableField arm (the developer’s method is the terminal hop’s materialization fact rendered by the shared table-expression switch; SqlRecordTableMethod permit retired; the runtime-stubbed empty-path/multi-hop shapes upgraded to classification-time rejections). Slice 3 stated the seam-worklist row-15 verdict (channel catch / early-return arms stay folded into the Fetcher on the service reentry path) and pinned its load-bearing premise as an enforcer (validateServiceTableField rejects a reentry service field carrying a present error channel), with root service leaves documented as value-level-re-fetch-without-site-level-re-query. Slice 4 moved the projected/discriminated DML follow-up SELECT into named rows<Name> companions (model-carried DmlTableField.reentryRowsMethodName(), minted through the registry; transaction boundary and no-match guard stay in the fetcher; SQL byte-equivalent), completing registry coverage of the whole family; the correlation stays keys-IN as recorded residue with named successor R489 (dml-reentry-values-join-rendering, which also records the RoutineWrite conditional). Slice 5 retired the validator’s dispatchPerformsReFetch mirror and landed the replacement reentry implementedness guard (site-level fact on a leaf outside BatchKeyField/DmlTableField → ValidateMojo error) in the same commit, no window. Acceptance held: R305 reentry execution tier green (SingleRecordPayloadDmlTest, SingleRecordTableFieldServiceProducerExecutionTest), @classified corpus classifying unchanged, level-1 closure oracle green throughout, bidirectional oracle passing for the reentry family. Gate review fixed three doc-level residues (an R-number leak in split-vs-inline.adoc per the workflow’s user-facing-doc check, the registry’s stale pre-slice-4 DML paragraph, a malformed javadoc indent in ChildField) and verified the full reactor green under mvn install -Plocal-db after remediating two sandbox artifacts unrelated to the diff (stale DB seed predating R458’s category_label, then a stale jOOQ schema-version marker stamped against the old seed). Independent-session In Review → Done review. Builds on R305/R316/R333/R431/R432/R461/R463; run-up R431 → R432 → this item held; feeds R471 (direct-SQL OnlyChild emit) and R489.

  • R482 (d482a79, follow-ons filed da0cc2c): Purge transient roadmap-item citations (R<n> / roadmap/<slug>) from every comment and javadoc region across the in-scope modules, and install a build-time guard against reintroduction. Roadmap items are transient (renumbered, shipped-and-gapped, or discarded), so a comment that leans on one is stale the moment the item moves; each citation was stripped with its prose kept (the default), relinked to a live {@link} where it stood for a symbol, or deleted only when the sentence carried nothing but the pointer, deliberately preserving any load-bearing design claim so the drift audit keeps its raw material. The payload is the guard: RoadmapReferenceScanner (a lexically-scoped detector that inspects only comment/javadoc regions and strips string/char/text-block literals, mirroring the roadmap-tool AsciiDoc-table block discipline), RoadmapReferenceGuardTest (a graphitron unit-tier meta-test that walks the in-scope module trees via a repo-root anchor and fails the build on any surviving citation, with a scanned-file floor so a drifted walk cannot pass vacuously), and RoadmapReferenceScannerTest (pins the detector contract: string/text-block/char literals not flagged, permanent-artifact slugs allowlisted, line-comment-ends-at-newline regression, planted reintroduction caught). The soft half is the CLAUDE.md "Javadoc conventions" rule. String-literal habitats stay out of scope: user-facing rejection/deprecation message slugs are R484, roadmap ids baked into generated javadoc via generator string literals are R491; the source-direct payload errors-object-slot migration surfaced by a stripped pointer is R490. Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db (guard tests RoadmapReferenceGuardTest 1/1, RoadmapReferenceScannerTest 11/11). Spawns R490/R491; separated from R483/R484.

  • R481 (f5b2eb5/f1515fa, In Review 3a67937, Spec → Ready 0be64fa): Multi-table polymorphic interface/union child fields honor the parent-projection contract, closing the R425 walk-omission family for the correlation reads their fetchers make off the parent row. Before, ChildField.InterfaceField/UnionField fell into the default -> {} arm of TypeClassGenerator.collectRequiredProjection, so a parent-holds-FK participant (self-FK navigate-to-parent, or a cross-table FK like store.address_id → address) whose parent-side correlation column no client selection projected crashed with IllegalArgumentException: Field "<fk-col>" is not contained in row type at single cardinality (gap A) or extracted a null DataLoader key on batched forms (gap B); every existing execution test masked the width of the gap by happening to select a field mapping the parent key. Fix introduces one orthogonal ParentRowDemand capability (a standalone List<ColumnRef> parentRowColumns() interface, mirroring BatchKeyField) carrying the whole projection demand, implemented by TableMethodField (its single-FK-hop source-side columns; the existing dedicated arm collapses onto the capability) and by the polymorphic InterfaceField/UnionField, cardinality-forked: single returns the union across participantJoinPaths of what MultiTablePolymorphicEmitter.singleBranchCorrelationWhere reads (KeyTupleWhere slot / JoinedCorrelation FK hop-0 slot sourceSide() columns; the parent bound key for a condition hop-0; On.Lateral hop-0 throws), list/connection returns parentSourceKey.columns() (the batched key extraction). Three walks consume it keyed on the capability, never on leaf identity: collectRequiredProjection (capability arm + record-source tripwire extended to ParentRowDemand), ParentProjectionContainmentCheck (capability-keyed requirement enumeration alongside the BatchKeyField one), GraphitronSchemaValidator.collectBaseNamedKeyColumns (capability arm replaces the TableMethodField case). Gap C (batched parent-holds-FK correlation, a genuinely absent capability) is closed at build time only: FieldBuilder.classifyParticipantRoute threads field cardinality and rejects DEFERRED, keyed to R487’s slug, a list/connection field whose resolved correlation reads a parent-side column outside the parent/hub table’s primary key (a condition hop-0 correlates on the bound key and is exempt), steering toward single cardinality where the relationship is single-valued. Coverage: ParentRowDemandTest (unit, the cardinality fork across the three correlation shapes), MultiTablePolymorphicParentHoldsFkPipelineTest (single-cardinality off-key classification, the gap C rejection on auto-discovery / explicit @referenceFor / multi-hop hop-0-on-parent arms, list child-holds-FK still classifies), MultiTablePolymorphicParentHoldsFkExecutionTest + a new parentRef fixture in MultiTablePolymorphicSelfFkOrientationExecutionTest (projection-independence proofs that select no parent-key-mapped field; the cross-table twin uses Store.contact over address/staff participants, whose nullable store.manager_staff_id gives a deterministic single-branch match on store 2, where a Customer union over two NOT-NULL FKs would be non-deterministic); no code-string assertions on generated bodies. Docs: one constraints sentence in polymorphic-types.adoc. Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db. Builds on R458; spawns R487 (batched-polymorphic-parent-holds-fk-correlation).

  • R458 (slice 1 cbb6eb1, slices 2-3 5968d53, rework 4bf1d0c, In Review 7fade80, Spec → Ready 482c693): Per-participant explicit join paths on single-cardinality multi-table interface/union child fields, the deferred capability R452’s rejection gate points at. A new repeatable @referenceFor(type: String!, path: [ReferenceElement!]!) on FIELD_DEFINITION states one participant’s join path from the parent table, overriding single-FK auto-discovery for the named participant only (participants left unnamed keep discovery); path reuses the @reference ReferenceElement grammar unchanged. The syntax consult rejected reviving @multitableReference (its routes: input-wrapper splices the type and path axes) and extending @reference/@discriminate (grain: the correlation is a fact of the (field, participant) pair, and repeated @referenceFor applications are keyed by type: and independent, the opposite of @reference’s concatenating chain, a contrast the reference page carries). Model: a two-arm sealed `ParticipantCorrelation on ChildField.InterfaceField/UnionField, decided once at classification: KeyTupleWhere(On.ColumnPairs) (the branch joins nothing, parent side is bound values, single form) for single-hop FK routes (auto-discovered, multi-FK-disambiguated, same-table self-FK), and JoinedCorrelation(List<JoinStep>) (the branch joins real tables) for multi-hop FK chains and any route carrying an On.Predicate hop; participantJoinPaths becomes Map<String, ParticipantCorrelation> dispatched exhaustively by all three cardinality emitters (single buildStage1Block, batched list, batched connection). Sliced: slice 1 shipped the directive, classification, carrier generalization, and the multi-FK / same-table-self-FK cases (both lower to KeyTupleWhere), turning R452’s rule 1b (same-table participant) and rule 1c (auto-discovery FK-count failure) messages into live steers to @referenceFor; slice 2 the multi-hop all-FK chains (declare one aliased local per intermediate hop, bridge back toward the parent via JoinPathEmitter.emitBackwardBridging, value-bind hop-0 against parentRecord/parentInput); slice 3 the condition correlation (join the aliased parent table bound to the parent’s key, apply the two-arg condition between parent and participant, intermediate filter: hops ANDed onto the branch WHERE). Slice-1 review fixed an orientation bug (every @referenceFor route was resolved with a hardcoded isList=false, so a list/connection field with a same-table self-FK route got the single-valued slot orientation, silently wrong rows; cardinality is now threaded into both explicit and auto-discovery resolution, pinned at pipeline and execution tiers) and filed the latent parent-holds-FK projection crash as R481. Slices 2-3 review found and the rework (4bf1d0c) closed a silent-wrong-data hole: a hop-0 filter: (a {key:, condition:} heading the path) was dropped, not rejected, in both arms (KeyTupleWhere carries only the FK column pairs; JoinedCorrelation’s `appendHopFilters/hopFilterTerms reads only intermediate hops i>=1), so FieldBuilder.classifyParticipantRoute now rejects a hop-0 filter structurally before the carrier fork, naming the participant and steering to a pure {condition:} first hop (which joins the parent alias) or an intermediate-hop filter; both shapes pinned at the pipeline tier. Docs: new referenceFor.adoc reference page with the constraints, the repetition contrast, and the hop-0-filter exclusion carved out (the Done pass corrected the constraints bullet’s meaning-inverting wording). Coverage: MultiTableChildReferenceForPipelineTest (17, classification + all structural/deferred/hop-0-filter rejections), execution-tier MultiTablePolymorphicJoinedCorrelationExecutionTest (multi-hop film -> film_actor -> actor, condition via an EXISTS-over-film_actor, auto-discovered inventory sibling keeping the field multi-table) and MultiTablePolymorphicSelfFkOrientationExecutionTest (list-direction self-FK on a category hierarchy). Independent-session review across slices; In Review → Ready → In Progress → In Review → Done. Full reactor green under mvn install -Plocal-db. Builds on R452 (the build-time gate + type lift, hard dependency); feeds R481. Out of scope: root-level (query) multi-table polymorphic fields (R382 / R76), per-participant fieldsJoin/orderBy emission (R76), and the connection-form and intermediate-hop-filter: execution fixtures (noted acceptable, the connection form shares the batched-list correlation path).

  • R486 (8aab6a6/c3c2be1, In Review 82846ff, Spec → Ready d1a8ba1): A roadmap-adjacent developer explanation space at roadmap/concepts/<slug>.html, intuition-first HTML background pages for dense or recurring roadmap concepts, distinct from both Diataxis trees and linking into them wherever a concept is covered as-built. The link plumbing is "one classification, two emitters": the target-kind knowledge inline in Main.mapAdocTarget is extracted into a sealed LinkTarget taxonomy (plus new ConceptPage and DeepDocsPath cases), the AsciiDoc emitter is refactored onto it behavior-identically, and a new HTML href emitter in ConceptPages formats the same classification for the concepts-page context, with a cross-Done fallback landing links to shipped (deleted) items on the changelog rather than a 404. The extraction was pinned first: 8aab6a6 captured characterization assertions for every pre-refactor mapAdocTarget branch (external, anchor, sibling, README, changelog, workflow redirect, arch quadrant-mapped and non-quadrant, top-level, legacy module, web-environment, unknown) so the round-trip table pins pre-refactor behavior rather than ratifying the refactor’s own output. ConceptPages also owns the data-concept-title contract (missing or blank fails generate, verify, and render-adoc alike, naming the file), the render-adoc staging step (hrefs rewritten repo-layout → site-layout, assets copied byte-for-byte), and the derived "Concept explainers" listings on the README roll-up and the status board (scanned from the directory, never hand-maintained, so verify catches listing drift). docs/pom.xml gains a <resources> entry carrying the staged pages into generated-docs. The R458 sketch page is refit onto shared concepts.css / concepts.js with the shared header, title attribute, derived-region fences (derived:status / derived:backlinks, owned by the skill’s refresh mode), and an em-dash sweep. The explainer skill encodes the page contract and the generate/refresh intents. Coverage: LinkTargetRoundTripTest (14, adoc direction), ConceptPagesTest (17, href direction + title contract + listings), all 104 roadmap-tool unit tests green; integration verified by a full docs build under which the WARN-fail asciidoctor render passes with the staged page and every href in it (items, changelog, index, manual, architecture, assets) resolves to a real file in generated-docs. Independent-session In Review → Done review. Out of scope and named as follow-ups: tool-injected backlinks at staging time (the drift-free successor to the authored backlink list) and fan-out to further concept pages via the skill.

  • R432 (3b873c3/9253bca/f6ebfaf/75f0b19, In Review 75f0b19, Spec → Ready df29e28): Collapse the four batched keyed-re-query leaves onto two source-gated ones, the R333 "First slice" beachhead and the first executable proof of the 2026-07-04 re-query unification resolution (full merge, laundered key). SplitTableField + RecordTableField merge into BatchedTableField and SplitLookupTableField + RecordLookupTableField into BatchedLookupTableField, each gated on a stored SourceShape (Table | Record) and spending R431’s types: the lift becomes total, the split arm now carrying KeyLift.FkColumns alongside its SourceKey(entryColumns, Wrap.Row) (the census established the stored difference was exactly one KeyLift lift component and the derived-method fork was emitsSingleRecordPerKey’s `|| LOAD_MANY disjunct, unreachable on the split arm). The gate is stored, not derived, since FkColumns is legitimately carried by both a table-row parent and a jOOQ-record-backed result parent. Six compact-ctor invariants pin the merge (BatchedTableField): total-lift residue agreement + carrier invariant (both arms), and Table-gated sourceShape==Table ⟹ FkColumns (checked-not-structural, the one-model discipline weighed against a second source-shape representation), Record ⟹ non-Connection, the R435 routine-surface pins + OnLateralArgs-nonempty-key guard, and Table ⟹ LOAD_ONE (deriveSplitQuerySource’s former prose guarantee made structural, which is what keeps the unified emitsSingleRecordPerKey formula behavior-identical on the split arm); the lookup leaf mirrors 1/2/3/6, with 4 deliberately absent (a Connection-shaped lookup is an author-reachable schema the validator rejects on both arms, not an unrepresentable generator state). The fresh names are load-bearing, not cosmetic: reuse would let existing narrowing sites (collectRequiredProjection’s `BatchKeyField arm, NESTED_WIREABLE_LEAVES) silently start receiving record-sourced instances, the exact R425 bug family, so every switch arm / instanceof / set membership is compiler-forced through the rename. Slice 1 (3b873c3) shipped the R333 parent-projection containment check ahead of the merge: ParentProjectionContainmentCheck cross-checks the audited collectRequiredProjection walk against an independent demand enumeration at the $fields emit site, keyed on BatchKeyField + sourceShape()==Table (never leaf identity, so slices 2-3 do not touch it) and walking the classifier’s flat field index with its own nesting worklist rather than borrowing the audited recursion (R425’s omission lived inside that recursion); a divergence throws IllegalStateException as a generator invariant (documented at the throw site as deliberately not a typed Rejection, per the Spec → Ready reviewer note), and the unit fire-case is the nesting-omission shape (the R425 family), not a bare set mismatch. Validator set-membership converts to source-gated predicates preserving the pre-merge asymmetries (Table-sourced absent from the LocalContext allow-list, Record-sourced not nest-wireable, RecordLookupTableField still absent from the LocalContext set). The fetcher fork stays behind one sourceShape seam (Table via buildKeyExtraction, Record via buildRecordParentKeyExtraction with the Outcome prelude + null-source guard); unifying the two renderings is R314’s re-platforming, deliberately left forked here so the beachhead does not become the invasion. Slice 4 (75f0b19) reframes the whole KeyLift axis in dispatch-axes.adoc per the reviewer note (from "record-parent lift provenance" to "how the key tuple is lifted off the held jOOQ record", leading the storage rationale with "a total lift removes an absence case and tells no lie" and R314 provisioning as the bonus), plus code-generation-triggers.adoc, the KeyLift/FkColumns/BatchKeyField javadoc, regenerated supported-schema-shapes.adoc + inference-axis-coverage.adoc, and R333’s leaf shipped-notes. Coverage: ParentProjectionContainmentCheckTest (the load-bearing acceptance, unit-proven able to fail on the recursion-omission shape and to pass across the corpus), SourceShapeProjectionTest (the stored-fact-vs-walk cross-check, now stronger than a leaf-identity tautology), GeneratorCoverageTest (dispatch partition exhaustive-and-disjoint), the pairwise-merged BatchedTableFieldValidationTest / BatchedLookupTableFieldValidationTest, renamed BatchedTableFieldPipelineTest, ReFetchDerivationTest, StubbedVariantPipelineTest, and the @classified corpus (renames-only, no verdict delta); no code-string assertions on generated method bodies. Acceptance held: generated output across the sakila corpus was byte-identical to the pre-merge baseline (diff -r empty) on slices 2 and 3, so execution-tier equivalence and fact-level classification stability hold trivially. Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db for the R432-bearing tiers (all graphitron unit/pipeline/execution/audit tests pass), with the sole failure the unrelated BgeEmbedderOnnxTest ONNX-native-binding infrastructure test in graphitron-mcp (a module R432 touches zero files in), flaking on degraded embedding numerics in the web sandbox exactly as the R13 changelog recorded. Depends on R431 (Done); is R333’s beachhead; feeds R314 (which retires the forked fetcher rendering) and keeps the R425/R426/R436 parent-projection regression suites green.

  • R431 (bab6f35/9c8261b/3229cbe/2ae8529/6c610e7, In Review bdb024c): Decompose the conflated six-component SourceKey (target, columns, path, wrap, cardinality, reader) onto the model’s separable facts, eagerly and mechanically, ahead of the R314 reentry re-platforming and as the type R432 depends on. Landed additive-then-cutover in four slices, each byte-identical against a clean baseline: slice 1 (bab6f35) deleted the denormalized target copy, its four readers migrating to a caller-supplied keyOwnerTable / the leaf’s own returnType.table(), with the null-target case landing as a typed ReturnTypeRef arm rather than a re-invented null; slice 2 (9c8261b) deleted path and retired the transitional JoinStep.LiftedHop onto a hop-less ParentCorrelation.OnLiftedSlots(targetTable, columns) arm (the four defensive unreachable emitter arms became type-level impossibilities, the HasSlots capability died into On.ColumnPairs, and the two empty-path compact-ctor invariants are now pinned structurally); slice 3.1 (3229cbe) moved the arrival-vocabulary half of Cardinality to a top-level Arity enum on the producer/carrier endpoint facts (deliberately not R463’s accumulated Arrival); slice 3.2 (2ae8529) shrank SourceKey to the (columns, wrap) residue (which earns the name), relocated the four live reader arms to the new sealed KeyLift fact (FkColumns / Lifter / Accessor(ref, Arity) / ProducedRecords(Arity)) carried by the three record-parent leaves and, as parentKeyLift, by InterfaceField / UnionField (the required parentSourceKey re-typing), dissolved the service arms into the MethodRef.Param.Sourced signature and ResultRowWalk into SingleRecordIdField plus the first-class SourceEnvelope enum, and deleted the Reader seal; slice 4 (6c610e7) consolidated the repeated bridging-join emit switch into JoinPathEmitter (R438 cleanup 2). Wrap is stored where authored (split/service) and derived via KeyLift.wrap() where inferred, with KeyLift.checkResidueAgreement as the constructor tripwire; the "hard" ResultRowWalk(OUTCOME_SUCCESS) => Wrap.TableRecord invariant is re-asserted (strictly stronger, unconditionally) at its named join site, SingleRecordIdField’s compact constructor. Docs: the `dispatch-axes.adoc chapter is rewritten onto the residue/lift/envelope/arity model with inbound xrefs retargeted. Coverage: unit-tier KeyLiftTest and SingleRecordIdFieldKeyShapeInvariantTest pin the migrated invariants, SourceKeyTest migrated alongside, no code-string assertions on generated bodies; the R425/R426/R436 pipeline and execution suites pin the migrated emit behaviors and stayed green. Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db (all 13 modules, 8176 tests, 0 failures). Follow-ups: R485 (fk-hop-narrowing-helpers, the thinly-coupled R438 cleanup 1, spun out as explicitly permitted when the item ran long) and R180’s deferred per-column accessor lift; feeds R432 and R314.

  • Discarded: Fold InputType and TableInputType under sealed parent InputLikeType (input-like-type-sealed-parent, R171): the item proposed tightening the input-type hierarchy with a sealed InputLikeType parent, the opposite direction from the dimensional pivot, which dissolves the input-type hierarchy entirely (R97 / R222 / R333). R222’s absorption ledger already recorded it as dissolving; with the team committed to R333 landing, keeping the item is noise that risks someone doing negative work against the pivot. Discarded per user decision 2026-07-15 rather than left as a superseded-pending Backlog entry. R222’s ledger line updated to note the discard.

  • Discarded: Support @tableMethod under a table-bound NestingField (tablemethod-under-nested-type, R277): @tableMethod is deprecated and withheld from the v1 surface (R400); @routine covers the pressing need it was chasing. Rather than build out nested-depth support for a directive that is not exposed (and that R288 separately flagged as an N+1 sync-fetcher defect this item would have entrenched), the @tableMethod support work is set aside to be redesigned when there is an actual feature request. Discarded per user decision 2026-07-15. R288 is narrowed to the polymorphic-interface N+1 case (a real correctness bug independent of @tableMethod); the @tableMethod inlining folds into the future redesign.

  • R202 (2d2ab74, In Review 02821dd, Spec → Ready 73e805d): Honor @field(name:) on an @error type’s extra fields (everything beyond path / message) at both the classify-time source-accessor check and the runtime property read. The accessor-coverage check called ClassAccessorResolver.resolve(sourceClass, sdlField.getName(), …) with the raw SDL name as the accessor base, so an extra field whose handler-source-class accessor diverged from the field name (getErrorCode() under code, SQLException.getSQLState() under state) failed classification with no author escape hatch. The directive is read once at the parse boundary and carried on the model to every consumer: GraphitronType.ErrorType gains a List<FieldAccessorOverride> component (FieldAccessorOverride(sdlFieldName, accessorBase), in SDL declaration order, List.copyOf-guarded, entries only for extra fields carrying the directive) populated by TypeBuilder.buildErrorType, plus a defaulting accessorBaseFor(sdlFieldName) that returns the override or the field name itself. Both check sites, FieldBuilder.checkErrorTypeSourceAccessors (class-backed payload / DML path via resolveErrorChannel) and R244’s walker.internal.HandlerAccessorCheck (@service outcome path), pass accessorBaseFor(...) instead of the raw name, so the remap rule lives in the model and the two bodies cannot drift; each reject string (and ErrorChannelWalkerError.HandlerSourceAccessorMissing, which gains an accessorBaseName component) appends a (remapped to '<base>' by @field) parenthetical so a failed override is diagnosable. Runtime wiring: GraphitronSchemaClassGenerator.buildErrorTypeFieldFetchers now takes the ErrorType and, after the path / message registrations, emits codeRegistry.dataFetcher(coordinates(typeName, sdlFieldName), PropertyDataFetcher.fetching(accessorBase)) per override, so the runtime read matches the classify-time check; non-directive extras keep resolving through graphql-java’s default PropertyDataFetcher by SDL name. buildErrorType rejects a present-but-blank @field(name: "") on an extra field and any @field on path / message (both synthesised by Graphitron, so the directive could never take effect). Stale javadocs on buildErrorTypeFieldFetchers and ErrorTypeFetcherClassGenerator claiming @error types are restricted to path + message are corrected. Docs: error.adoc gains an "Extra fields" section, field.adoc cross-links it, and the @field docstring names the @error extra-field accessor axis; the prose is scoped to the source-accessor read only (R201, the payload-construction half, is still Backlog) so no half-implemented behaviour is advertised. Coverage: pipeline-tier positive remap on both check sites (@service via getLocalizedMessage(), class-backed payload path), remapped-but-still-missing reject naming both field and directive value, three parse-rejection enum cases (blank / path / message); execution-tier round-trip of a divergently-named getAttemptedId() through FilmLookupInvalid.attempted (-7), the first execution coverage of @error extra fields. Independent-session In Review → Done review; full reactor green under -Plocal-db (all 13 modules, 593 graphitron-sakila-example execution tests). Out of scope: R244’s pending consolidation of the two check sites, and R201’s payload-construction site (ship together or back-to-back; the read/construct asymmetry persists until R201 lands).

  • R479 (9ddf1d7, In Review 56f009b, Spec → Ready 76e1136): Stop a malformed connection after/before cursor from redacting into a correlation-id 500 and give it the same clean, message-bearing client error a bad node id and a bad page-size argument already get. The emitted ConnectionHelper.decodeCursor turned client-supplied wire input into Field<?>[] seek values with no guard, so bad Base64 (IllegalArgumentException), a token count below the order-by arity (ArrayIndexOutOfBoundsException), and a non-coercible token all threw raw runtime exceptions the no-channel disposition redacted, inconsistent with sibling pageRequest (R415) and the single-record node-id decode. Fix wraps decodeCursor’s decode body (Base64 decode, split, token loop) in one try whose narrow multi-catch (IllegalArgumentException | org.jooq.exception.DataTypeException) classifies blame at the wire boundary: every pure-function-of-client-input failure collapses to one `GraphitronClientException("cursor is not valid (was: \"<echo>\")") (echo capped at 100 chars with a trailing …, following R415’s (was: ...) convention), while any other unchecked throw (e.g. an NPE from a buggy custom Converter) stays a genuine server fault and keeps propagating to the redacted 500. Arity is strict in both directions (tokens.length != orderByColumns.size()), rejecting over-split cursors that were previously silently tolerated: encodeCursor emits exactly N NUL-joined tokens and PostgreSQL strings cannot contain NUL, so any other count is a forged/corrupted/stale-across-schema-change token the generator never emitted. Spec-amending discovery landed in the same commit: jOOQ 3.20.11’s deprecated DataType.convert(String) is lenient and returns null for an uncoercible token rather than throwing DataTypeException (verified across numeric, temporal, UUID, and the MpaaRating enum converter), so failure mode 3 would have been a silent defect (seek on a null bound value); the loop therefore also rejects a non-sentinel token that converts to null (a genuine SQL NULL is the \u0001 sentinel, handled in the branch above), with the DataTypeException clause retained as documented-defensive. No router or fetcher-emitter changes: pageRequest is decodeCursor’s only caller and every connection flavour (standard, dynamic-ordering, polymorphic) funnels through it, and R415 already unified the no-channel disposition on both sync and async arms, so the marker surfaces on root and nested (DataLoader) connections alike. Coverage: execution-tier `GraphQLQueryTest next to the R415 guards, each pinning the exact message and doesNotContain("An error occurred. Reference:"): bad Base64 (after), non-coercible token through the null-guard on numeric FILM_ID, under-split on the two-column filmsByRateDescTitleAsc, over-split strictness (new behaviour), and the before variant; existing round-trip tests pin that legitimate cursors are unaffected. Class-javadoc and generator-comment prose gain the rejection contract. Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db (all 13 modules, 593 graphitron-sakila-example execution tests, 0 failures). Builds on R415; this is the exact "malformed cursor redaction, same family, different surface" gap the R415 changelog named as filed nowhere. Out of scope and untouched: capping the node-id path’s uncapped echo (parity cleanup), R476’s totalCount redaction bypass, and a configurable max page size.

  • R478 (d19a4a6, Spec 289d7ce): Seal KeyAlternative from an enum-carrying record into a sealed interface with Direct/NodeId variants, each carrying its requiredFields/columns contract as structure rather than the unenforced javadoc prose R477 lived in. Direct stores List<RepBinding> (rep field, column) pairs, so "sizes equal, index-by-index" becomes unrepresentable rather than a promise; requiredFields()/columns() unzip the pairs in order. NodeId carries expectedTypeId (the resolved wire prefix) plus columns (the decode arity), with requiredFields() a derived constant ["id"]. HandleMethodBody.emitDecodeAndGroup becomes an exhaustive sealed switch with no default arm (a future third key shape is a compile error at the fork), and the NODE_ID arm’s emit-site prefix re-derivation with its dead typeName fallback deletes: the decision moves to EntityResolutionBuilder, which builds NodeId(nt.typeId(), ...) directly. Single-sources the typeId fact by dropping EntityResolution.nodeTypeId; EntityFetcherDispatchClassGenerator.buildTypenameForTypeIdMethod derives its reverse map from each entity’s NodeId alternative (presence sets coincide with the former nodeTypeId != null test, inclusion ignoring resolvable, key is expectedTypeId) and sheds its now-unused KeyShape import. Also corrects the stale GraphitronType.NodeType.typeId javadoc (never null on a classified NodeType; defaulted to the type name at classify time). Pure model refactor: generated output byte-unchanged, no pipeline-snapshot or execution-test edits, so the churn is KeyAlternative, its consumers/builder, EntityResolution, GraphitronType, and two test files (EntityResolutionBuilderTest rewritten to instanceof + expectedTypeId/bindings pattern assertions plus a NodeId.requiredFields()==["id"] pin; a RejectNonIdNodeIdPipelineTest comment). Depended on R477 (now Done), whose behavioural arity guard this refactor preserves verbatim. Independent-session In Review → Done review; full reactor green under -Plocal-db (all 13 modules, 588 graphitron-sakila-example execution tests). Out of scope and untouched: any behavioural/emitted-code change, routing the batch decode through the per-type decode<TypeName> helpers, and new key shapes (this item only prices their arrival as a compile error at every fork site).

  • R474 (2efbc47, Spec 5d245ef/04f2645): Adopt mvnd (Apache Maven Daemon) 1.0.6 in the Claude Code web dev environment to cut per-command Maven JVM warmup. A new idempotent SessionStart prereq step in .claude/scripts/session-start-web-env.sh fetches mvnd from downloads.apache.org to /opt/mvnd and symlinks /usr/local/bin/mvnd, failing open to plain mvn on any error (simulated download failure leaves no /opt/mvnd residue and the hook continues); the R439 background warm build now runs through mvnd when the install succeeded, leaving a hot, JIT-warm daemon (3 h idle timeout) so the session’s first foreground command is fast rather than paying a cold daemon start. The PreToolUse guard (wait-for-web-env.sh) match widens from grep -qw mvn to grep -qwE 'mvnd?' so an mvnd command issued during the warm build cannot race it (catalog-jar clobber included). Parallel test tolerance becomes an enforced invariant rather than a sandbox-only aspiration: the CI verify job switches to mvn verify -Plocal-db --batch-mode -T 1C, so a test that fails only under module-parallel execution fails at the gate and the fix is the test, never serializing the build; web sessions run mvnd’s default -T <cores-1> with tests on, keeping sandbox and CI on one execution shape. Docs: CLAUDE.md gains prefer-mvnd-in-web-sessions guidance, .claude/web-environment.md gains the mvnd section with the accepted quirks (Maven 3.9.16 skew vs /opt/maven 3.9.11, -q stdout buffering under mvnd, SNAPSHOT-plugin classloader-cache exclusion, ~1.35 GB daemon RSS) and states mvn/mvnd command interchangeability explicitly. No pom changes; pure accelerator. Independent-session In Review → Done review; full reactor green under mvnd verify -Plocal-db -T 1C in-sandbox (all 13 modules, 588 graphitron-sakila-example execution tests passing while siblings built concurrently) and on trunk CI with -T 1C. Out of scope: within-module surefire/JUnit parallelism, mvnd for local development or as a repo-pinned driver, mvnd 2.x / Maven 4.

  • R13 (673535a/9732135/896b105/1e15985/795786a + rework 2c8aac4, review 32169ba): Faceted search on @asConnection via a new @asFacet directive on filter-input fields. Each marked field becomes an entry on a synthesised <ConnName>Facets object attached as a nullable facets field on the generated Connection type, with per-(scalar, nullability) <Scalar>FacetValue value types (FacetNaming keys the derived name on both so nullable and non-null facets over one scalar never collide; value mirrors the filter element’s type exactly so facet values round-trip into the filter with no coercion). Synthesis rides ConnectionPromoter’s field-first walk; the resolved `List<FacetSpec> lands on GraphitronType.ConnectionType as a denormalized view carrier (contained route: Operation.Facet stays unpopulated behind the ConnectionType quarantine for R314 to fold in, matching the totalCount precedent), and new FacetsType / FacetValueType sealed arms ride the per-type emit with all exhaustive switches updated. At runtime the root Query connection fetcher assembles a facet plan on ConnectionResult from generated filter-minus-self fragments (<field>FacetBaseCondition over non-facet fields via null-literal suppression keyed on (argument, field) extraction identity, <field>Facet_<g>Condition per facet), and ConnectionHelper.facets(env) issues one UNION ALL of per-facet GROUP BY arms (each under base AND every other facet’s predicate, IS NOT NULL scrub on non-null facets, TEXT-unified value column decoded back through the column’s DataType), lazy on selection, sorted post-decode on count DESC then the typed value’s natural order. The facets delegate routes failures through ErrorRouter.surfaceClientErrorOrRedact so a facet SQL failure degrades to facets: null plus a redacted error while the page resolves. rejectFacetMisuse (definition-keyed half extracted to FacetFieldValidation, one predicate shared with the promoter so the passes cannot drift) rejects every v1-unsupported shape at build time: non-@field bindings, @reference/@condition/@nodeId co-occurrence, ID and input-object values, non-null fields, connectionName: override, unreached inputs, non-root/polymorphic/structural-only carriers, and facet-name collisions across the carrier’s whole filter surface. Independent-session In Review → Ready → In Review → Done review: the first pass reproduced a consumer javac break (undeclared lifted filterMap local in the base fragment on facet-minority filters) and a wrong-counts defect (suppression ignored the outer argument name, folding a same-named sibling-arg filter into the facet’s own predicate); the rework fixed both (the second both by identity fix and by build-time rejection), pinned the NULL bucket and scrub at the execution tier over an all-NULL seeded column, replaced FacetEmitterTest’s banned code-string assertions with method-surface checks, and pinned the failure/redaction contract end to end. Full reactor green under `-Plocal-db on the approval pass (one unrelated BgeEmbedderOnnxTest flake, passing in isolation and on resume). Spawned R475 (duplicate Java parameter names when two filter args share a field name, pre-existing) and R476 (totalCount shares the unredacted-error gap). Covers GG-335; resolves SOPP-141. Hierarchical facets (Phase 6), reference-path facets, and bounded-domain shape F remain follow-ups.

  • R461 (03a5b25, In Review 0b9308f, Spec fe68a59): Unify the four divergent SDL-field-to-Java-accessor resolution implementations behind one candidate model. ClassAccessorResolver.enumerate becomes the single home for the name rules, the boolean-only is-gate (on the member’s own return type), the member filter (public, non-static, non-bridge, non-synthetic, not Object-declared), and the per-kind parameter-shape rule; it is parametric in the CandidateKind set a consumer accepts so each reduction’s switch is exhaustive over only the arms it can receive. Two reductions consume it: resolve keeps its signature and first-match reduction (now picking up the bridge/synthetic/Object filter it previously lacked, a latent-bug fix, B6) and FieldBuilder.collectAccessorMatches keeps its table-identity/cardinality ambiguity reduction (zero-arg kind only), with the @sourceRow-hint ambiguity rejection preserved. A new discovery-direction probe (sealed AccessorProbe Grounded | NoMatch, never a nullable Type) replaces RecordBindingResolver.findAccessorReturnType and inferAccessorName, which are deleted: the result/input propagation and the R329 producerBindLevel presence probe all call it once per field under @field(name:)-resolved base names and the field’s real argument shape (B2/B3/B4/B5/B8). forBackingClass single-sources the walk’s class-shape→candidate-order rule, pinned equal to the emission-side variant derivation by an order-bridge meta-test over TypeBuilder.resultVariantKindFor. A reason ledger records gated accessor near-misses so the dangling-type-reference backstop (GraphitronSchemaBuilder.rejectDanglingTypeReferences) names the accessor gate for a sole-producer type rather than emitting a generic no-producer cascade. Coverage: unit-tier ClassAccessorResolverTest (order/is-gate/filter/arity/field-fallback table, covariant-bridge skip, order-bridge meta-test) and pipeline-tier R461WalkGroundingPipelineTest (B1 POJO+record order, B2 arity incl. sole-producer arity-gate rejection, B3 rename, B4 sole-producer boolean-gate, B5 field fallback incl. sole-producer field-fallback-with-args rejection, B6 covariant, inheritance, both B8 carrier-discrimination flips); no code-string assertions on generated bodies. Independent-session In Review → Done review; full reactor green under -Plocal-db. Follow-up to R180’s deferred lift; routing the LSP’s CatalogBuilder.beanAccessorSlot through this resolver (audit finding M19) is left as a separate item.

  • R463 (e9e3313): Consume R279’s ancestor-cardinality rider ; compute the true ancestor-product arrival fold and populate Source.OnlyChild where the product is One, retiring the conservative hard-coded Source.Child on every ChildField. New Arrival (lattice One < Many, Many absorbing, tensor product) and ArrivalIndex, a pure typename-keyed fold over the assembled (pre-connection-promotion) SDL: a composite type is Many if it carries a @node/@key seed or is reached by more than one field edge (fan-in / recursion, which subsumes the fixed point since a reachable cycle implies a second reaching edge), else its single reaching edge’s parent arrival tensored with the edge’s raw SDL list-ness; roots are the empty product (One). Structural edges (interface→implementor, union→member) propagate arrival unchanged while still counting toward the multi-edge test. Computed once in GraphitronSchemaBuilder and threaded onto GraphitronSchema (parse-boundary containment: no graphql-java type survives into the stored Map<String, Arrival>); OutputField.source() becomes the storage-free derived view source(Arrival) (root leaves ignore the argument), read through the single GraphitronSchema.sourceOf(coordinate) seam so arrival stays a parent-typename-grain fact and is never copied to the ~25 leaves. requiresReFetch() reworked to read the leaf’s own sourceShape directly (arrival-agnostic, behaviour-identical), and the GraphitronSchemaValidator re-fetch-mirror diagnostic reads sourceOf. Model + tests only: emitters keep leaf-identity dispatch, so an OnlyChild-classified batch field still emits its one-element DataLoader and generated output is byte-unchanged; the direct-SQL OnlyChild emit and its enforcer ride the R431→R432→R314 chain (forward obligation recorded). The Source.OnlyChild javadoc carries the honesty clause (One is a static per-dispatch guarantee about unaliased projections; the arm must stay row-correct at every arrival count). Coverage: WrapperAlgebraTest pin lifted to the Root law + parent-grain law + a coverage floor observing OnlyChild/Child/Root; ClassifiedDslTest.SOURCE_KNOWN_GAPS loses its OnlyChild exemption (now empty); corpus rows flip to OnlyChild where the fold yields One, plus seven new hand-asserted edge-case fixtures (deep single chain → One, list ancestor → Many, fan-in of two single edges → Many, recursion → Many, @node-seeded → Many, connection ancestor → Many, single mutation payload carrier → One); no code-string assertions on generated bodies. Independent-session In Review → Done review; full reactor green under -Plocal-db. Builds on R305/R316/R317; consumes R279’s rider; R308 landed first and retargeted the three forward references here.

  • Discarded: Graphitron MCP server programme (graphitron-mcp-server, R118): the programme is delivered; the anchor has nothing left to coordinate. All ten core slices shipped as their own items (R341 transport skeleton, R361 shared-model seam, R372 RAG foundation, R385 docs.search, R386 catalog.search, R428 execute tool, plus the catalog/code/schema/diagnostics tools, the edges tool with reverse index, and the directives resource): the registered surface in GraphitronMcpServer is catalog.tables, catalog.describe, catalog.search, services, conditions, records, schema, diagnostics, edges, docs.search, execute, and the directives resource. The one live open question (English-only bge-small-en-v1.5-q vs Norwegian catalog comments) is carried to R470 (multilingual-catalog-search-embedding). The optional slice 11 (semantic method search) is consciously not re-filed: file it under the dev-loop theme if agent round-trips ever show the structured services/conditions/records tools are not enough. The programme body’s architecture spine (structured vs semantic split, one-model-two-views LSP integration, stability gradient, cross-cutting principles) is embodied in the shipped code and its per-slice changelog entries.

  • Discarded: roadmap-tool verify tripwires throw BuildFailure, not System.exit (roadmap-tool-tripwire-buildfailure, R394): already delivered without the item being closed. BuildFailure exists and all four verify tripwires throw it (Main.runVerify README drift, Main.validate front-matter, LeafCoverageReport.run drift/no-traces, AdocMarkdownTableCheck.run findings), plus the later-added DirectiveSupportReport guard; CLI/usage/create-file-exists paths correctly remain on System.exit, exactly as scoped.

  • Discarded: capture test stdout/stderr to per-class files via Surefire redirectTestOutputToFile (surefire-redirect-test-output, R373): already delivered without the item being closed. redirectTestOutputToFile=true is set in both the Surefire and Failsafe configurations of the parent pom, and the graphitron-mcp logback-test.xml WARN clamp was relaxed to root INFO with a comment citing the redirect; both scope points satisfied.

  • Discarded: render Mermaid diagrams on the published docs site (render-mermaid-diagrams-on-docs-site, R326): the acceptance end-state (mermaid blocks render as inline SVG on the site) was reached by a different mechanism than the item proposed. Instead of an asciidoctorj-diagram/kroki server-side backend, docs/_theme/docinfo-footer.html ships a client-side mermaid.js module loader that rewrites code[data-lang="mermaid"] listings (the [source,mermaid] form roadmap-tool emits) into rendered diagrams in the browser, sidestepping the CI-backend cost the item worried about.

  • Discarded: add a typeName-first decode-helper entry point (decode-helper-typename-first-resolution, R263): speculative with no consumer, per the 2026-07-13 staleness audit (§A). resolveDecodeHelperForTable now resolves through the @node-only NodeIndex and rejects multi-node ambiguity at validate time rather than silently picking decode<firstType>; both callers pass the table name as primary and none routes an authoritative @nodeId(typeName:). R377 settled the open question the item hedged on. Finding for the record: the fallbackTypeNameOrTypeId argument is still consulted only on the empty branch, so a future typeName-first caller must not expect it to drive the suffix; re-open if a MUST-route @nodeId(typeName:) caller appears.

  • Discarded: drop or wire FkJoin.alias dead storage (fkjoin-alias-dead-storage, R120): premise dissolved by R438’s join reshape. The named types are gone (FkJoin/ConditionJoin replaced by JoinStep.Hop/LiftedHop), and the stored alias is no longer dead: Hop.alias() is consumed extensively by the routine/chain join emitter in TypeFetcherGenerator, i.e. the item’s "use it" fork happened as a side effect of the reshape. This resolves residue point 2 of R443, whose body is trimmed accordingly. Residual observation, cosmetic and not re-filed: JoinPathEmitter.generateAliases still derives its own per-hop aliases for the inline emitters instead of consuming Hop.alias(), a dual derivation with no generated-code difference.

  • R465 (a9deae5, Spec c3ae081/145afd1): Bump the two graphql-java satellite artifacts in the root pom dependency-management block, federation-graphql-java-support 6.0.0 → 6.2.0 and graphql-java-extended-scalars 22.0 → 24.0, holding graphql-java itself at 25.0 (the 25 → 26 jump breaks generator compilation and outruns what the two satellites officially support; deliberately out of scope as the conservative, ready-now half of the graphql-java catch-up). Federation 6.2.0 (built against graphql-java 25.0) and extended-scalars 24.0 both resolve cleanly and introduce no failures. R464 having already removed the convention table and its drift guard, the six new extended-scalars constants (YearMonth, Year, AccurateDuration, NominalDuration, SecondsSinceEpoch, HexColorCode) need no map-or-exclude curation: the reflective @scalarType resolver binds any public-static GraphQLScalarType constant on the classpath automatically, so they become resolvable candidates with no code change. Pure two-line version bump; no generator, generated-output, or test changes, exactly as scoped. A tree-wide grep confirms no live source, test, or user-facing doc still references the deleted convention table or its conventionTable_coversEveryExtendedScalarsField drift guard. Independent-session In Review → Done review; full reactor green under -Plocal-db across all 13 modules.

  • R346 (ab70c35/41c0b70): Regenerate and guard the generated supported-directives.adoc migration fragment against directive-set drift. The fragment (included by migrating-from-legacy.adoc, emitted by directive-support --mode=migration) promised in its own header a "verify-mode CI guard" that never existed, so it drifted silently every time a directive changed. The directive-support CLI gains an additive --verify flag mirroring leaf-coverage --verify: it regenerates the fragment in memory, compares against the committed --output file, and throws BuildFailure with a copy-pasteable REGENERATE_COMMAND on any drift (--verify requires --output, else exit 64). To keep the verify path inside the rewrite tree (standalone-build principle), the frozen 25-directive legacy set is snapshotted verbatim from graphitron-common/src/main/resources/directives.graphqls at tag v9.3.0 (pre-R182 delete) into roadmap-tool/src/main/resources/legacy-directives.graphqls rather than read from the legacy module; a stripped-before-parse provenance header documents the source. A phase-bound verify-supported-directives execution in roadmap-tool/pom.xml runs the guard on every mvn verify (PRs included), alongside verify-roadmap-readme and check-adoc-tables, so drift fails on the PR that introduces it rather than after merge. The one-time regeneration corrects the generated Supported-directives prose (R346 decision 2): it drops the unbacked "exercised by …​ a test fixture" claim (migration mode never computes that signal into the list, which gates on rewriteByName.containsKey minus the rejected/withheld sets) and states the real criterion (declared + supported, documented in the architecture chapter). Coverage: DirectiveSupportReportTest gains the prose assertion plus the --verify round-trip (zero on match, BuildFailure on drift, exit 64 without --output); the guard is its own regression net. The sibling supported-schema-shapes.adoc fragment (identical unguarded gap, but a build-trace-dependent guard shape) is deferred to a Backlog follow-up. Independent-session In Review → Done review; guard verified to fail cleanly on a deliberate fragment edit; full reactor green under -Plocal-db.

  • R464 (7786ce4, Spec f79855e/b1ff238): Remove convention-based scalar resolution; make @scalarType(scalar: "fully.qualified.Class.FIELD") the single explicit way to bind any non-spec, non-federation scalar. Generator side is pure deletion: ScalarTypeResolver loses CONVENTION_TABLE, conventionTable(), and resolveByConvention(), and TypeBuilder’s classification ladder loses its convention rung so a directive-less non-spec/non-federation scalar falls straight to the `@scalarType-pointing hard error (message simplified to name the directive as the single fix). This deletes the classpath-dependent classification surprise (a transitive extended-scalars pull silently resolving a bare scalar BigDecimal with no directive and no intent) and retires the convention-table drift test that gated extended-scalars / graphql-java upgrades (it tripped on the extended-scalars 24.0 dry run and blocked R465). The five spec built-ins and the federation-namespace scalars are untouched. Rather than dropping editor support, the LSP completion is upgraded: ClasspathScanner gains a readScalarConstants pass reading public static GraphQLScalarType fields by exact JVM descriptor match (the JOOQ_CONDITION_DESCRIPTOR idiom; final deliberately not required since the reflective resolver binds a non-final constant just as well), surfaced on a new CompletionData.ExternalReference.scalarConstants slot (with a back-compat 5-arg constructor keeping ~20 test callers compiling); ScalarTypeCompletions now completes className.fieldName from that scan, so it offers the consumer’s own scalar constants (com.example.Scalars.MONEY) plus any library’s with no coupling to extended-scalars, preferring a case-insensitive field-name match for the enclosing scalar X. The sakila scalar BigDecimal and the GraphitronSchemaBuilderTest R355 fixtures gain explicit @scalarType; the two CONVENTION_LAYER classification cases are deleted, the single-path UnclassifiedType contract case survives (asserting the new message so a re-added fallback fails loudly), and the old DIRECTIVE_BEATS_CONVENTION case is retained reframed as DIRECTIVE_ALIASES_TO_DIFFERENT_CONSTANT. Coverage: scanner test pins the public+static+GraphQLScalarType filter with three negatives (wrong field type, non-static, non-public), rewritten LSP completion + text-edit tests feed a populated CompletionData and assert the className.fieldName items, field-name-first preference, case-insensitivity, and the empty-on-other-directive guard; no code-string assertions on generated bodies. Docs sweep: custom-scalars.adoc, scalarType.adoc, and code-generation-triggers.adoc drop the convention layer, and resolution order is now two implicit paths (spec built-ins, federation-namespaced) plus the directive. Pre-1.0 breaking change; the actionable error message points at the mechanical fix. Independent-session In Review → Done review; full reactor green under -Plocal-db. Unblocks R465.

  • R459 (ed5b79b): CompileDependencyGraphBuilder now models the schema-shape → fetcher wiring edge for fetcher-owning plain-object nesting types. A nested type that owns a fetcher (any classified nested field, per R303) emits a <Type>Fetchers class its <Type>Type schema-shape wires (FilmMetaType -> FilmMetaFetchers); the builder registered only the schema-shape node, so the wiring loop never added the edge and the TypeSpecReferenceWalk completeness oracle (correctly) flagged the superset gap. Fix is a dedicated addNestedFetcherNodes() walk (called from build() before addBlanketAndWiringEdges) that mirrors TypeFetcherGenerator.collectNestedFetcherClasses’ reachability verbatim: iterate `TableBackedType roots, walk each NestingField tree, dedup nested types by name, recurse into inner NestingField`s unconditionally, and register `units.fetchers(name) gated on a mirrored nestedTypeOwnsFetchers predicate (no FetcherEmitter coupling, same discipline as the builder’s filtersDecodeNodeId / hasSqlGeneratingField mirrors). Registering the node is the whole fix: the existing wiring loop then supplies schemaShape -> fetcher, schemaClass -> fetcher, and the blanket edges for free. Coverage: unit CompileDependencyGraphBuilderTest.fetcherOwningNestingTypeRegistersFetcherNodeAndWiringEdges and a pipeline IncrementalCompileHarnessTest corpus extension (Film.meta: FilmMeta { language: Language @reference }, single-valued inline TableField that exercises exactly the wiring edge and not the deferred per-field gap). Independent-session In Review → Done review verified the fixture is non-vacuous (disabling the walk reddens the oracle with exactly the one FilmMetaType -> FilmMetaFetchers gap, nothing else); full reactor green under -Plocal-db. The nested fetcher’s own outgoing per-field edges are deferred to Backlog R462 (nested-fetcher-outgoing-field-edges).

  • R457 (7588358, core c2e818b, cutover c41752d, gate-fix 376ecd8, In Review e83532b): @mutation(table:) names a @mutation(typeName: DELETE) field’s write target on the consuming field, retiring @table-on-input for DELETE and making R332’s deprecation warning actionable. A DELETE commonly returns a bare ID / Boolean / count and can never return the deleted row’s @table type (R287 rejects DELETE → @table at authoring time, backstopped by MutationDeleteTableField’s compact constructor), so the only place to name the write target is the field: `deleteFilm(in: FilmDeleteInput!): ID @mutation(typeName: DELETE, table: "film"), the field-level analogue of @service(argMapping:). Precedence is @mutation(table:) (preferred) > the input’s @table (deprecated migration bridge, silently outranked when both are present, never cross-checked). The spec’s rung 1 (return-derived table) was dropped by author/user agreement precisely because R287 makes it unbuildable for DELETE; rungs 2 (override) and 3 (bridge) ship and fully satisfy the goal. Mechanism: directives.graphqls gains table: String on @mutation (documented DELETE-only); DmlWalkerInputArgResolution gains a RawArg arm making "the single input arg is not a TableInputType`" a normal outcome rather than an immediate reject, with UPDATE translating it back to the pre-R457 rejection verbatim (byte-identical) via `rawArgUpdateRejection while the DELETE classifiers own the fallback; FieldBuilder.resolveDeleteWriteTarget resolves the table by precedence and re-derives the input fields through TypeBuilder.resolveInputFields (factored out of buildTableInputType so both routes share one classification loop, returning the narrow (fields | failure) fact, not a synthesized TableInputType); the R330 validator-bypass is closed by mirroring GraphitronSchemaValidator.collectInputFieldRejections at the field-derived call site (a field-derived input never lands in the registry validateTableInputType walk); table: on a non-DELETE verb rejects loudly with the typed, sealed MutationTableArgError.UnsupportedVerb (stable graphitron.mutation-table-arg.* LSP code), with the classifier and mvn graphitron:validate reading one TABLE_ARG_SUPPORTED_VERBS set. Commit 1’s R332 DELETE carve-out is repurposed at cutover (additive-then-cutover, no dead set) from suppression to selecting the DELETE-specific replacement wording, so the warning now names @mutation(table:). Coverage: pipeline-tier MutationTableArgClassificationTest (7 cases: byte-identical carrier vs @table-on-input, no-@table classifies, unknown-table reject, no-write-target message leads with @mutation(table:) and cites R287, INSERT/UPDATE unsupported-verb rejections, validator-mirror parity on both paths), TableOnInputDeprecationWarningTest, execution-tier FixtureWarningsGateTest (FilmDeleteInput warns naming @mutation(table:)), and the deleteStorageBinByCode round-trip in DmlBulkMutationsExecutionTest (the sakila fixture dropped @table and set @mutation(table: "storage_bin"), round-trips identically against PostgreSQL); drift guards RejectionSeverityCoverageTest + SealedHierarchyDocCoverageTest; no code-string assertions on generated bodies. Docs: mutation.adoc (signature, parameter table, "Naming the DELETE write target" section, reworked constraint bullet), table.adoc WARNING, deprecations.adoc, code-generation-triggers.adoc, typed-rejection.adoc. A DELETE-scoped slice of R97’s "the write target is the consuming field’s property" axis; R97 remains the home for the general @table-on-input removal. Independent-session In Review → Done review; full reactor green under -Plocal-db (MutationTableArgClassificationTest 7, TableOnInputDeprecationWarningTest 4, FixtureWarningsGateTest 2, DmlBulkMutationsExecutionTest round-trip all pass).

  • R455 (f1d93a8, workstream B 319e668, In Review 991c2ae): Fix the TypeSpecReferenceWalk blind spots that silently falsified the R410 incremental-compile completeness oracle’s superset guarantee (walkEdges(u) ⊆ modelGraph.directReferences(u)), so the dev-loop incremental compiler could prune a dependent an ABI change should have recompiled while the oracle stayed green. Two coupled workstreams, landed model-first so every trunk commit stays green. Workstream A (graphitron-javapoet): TypeSpec.referencedClassNames() now descends $L args that are a nested CodeBlock / anonymous-class TypeSpec / AnnotationSpec (a $T stored opaque in a $L block was previously lost, missed across 60+ emitter sites) and walks type- and method-level type-variable-bound declarations, guarded by an identity-visited set so self-referential bounds (T extends Comparable<T>) terminate; the javadoc is corrected so the sole remaining blind spot is a class name baked into a raw string ($L String / $S). Workstream B (CompileDependencyGraphBuilder): a separate top-down projection walk mirroring TypeClassGenerator’s emit seam models the type-to-type projection-composition edges that were entirely absent (types.Film → types.Language` for each inline TableField/LookupTableField composing Target.$fields(...)), attributing every edge to the hosting type class so nesting-hosted fields land on the outer type (not their immediate parentTypeName()), reaching NodeIdEncoder precisely when an inline filter decodes a @nodeId argument, adding the generated <Type>Conditions edge for a GeneratedConditionFilter, and blanketing the frozen GraphitronClientException scaffold. The per-child dispatch is a no-default exhaustive switch over the ChildField leaves carrying the one-model drift guard (a future inline-projecting leaf fails to compile until its edge is declared). TypeSpecReferenceWalk’s javadoc documents the shrunk review-only residual (same-package raw code-bearing string) with a discovery recipe; net 2’s FQCN scan is kept as-is (a same-package simple-name literal scan would over-collect). The demonstrated false green (Film → Language` inline projection on the R410 harness) is now true-green, and the harness corpus is extended with an inline @nodeId-decoding list reference (Language.films) exercising the typeClass → NodeIdEncoder, → conditions, and same-package nested-$L projection edges end-to-end. The fetcher-owning-nesting-type wiring gap the fix would otherwise surface is filed separately as R459. Coverage: 7 TypeSpecReferencedClassNamesTest unit tests (nested $L at depth ≥ 2, anonymous class, annotation, type/method type-variable bounds, recursive-bound termination, raw-string residual), 4 new CompileDependencyGraphBuilderTest unit tests (projection target, GraphitronClientException blanket, NodeIdEncoder only-when-decoding, nesting-hosted attribution to the outer class), and the extended IncrementalCompileHarnessTest; MultiSchemaPipelineTest’s R78 guard gets deeper coverage for free and stays green. Independent-session In Review → Done review; oracle + builder + multi-schema suites green under `-Plocal-db, javapoet unit tests verified passing on demand.

  • R456 (80a3df7, In Review e8571cc): Guard WorkspaceFile source/tree reads against a concurrent didChange edit/swap/close. The LSP handed the live, mutable WorkspaceFile out of its lock (Workspace.get) and let the five async request handlers (hover, completion, codeAction, definition, inlayHint, all on ForkJoinPool.commonPool) plus the diagnostic-recalc drain walk its tree-sitter tree and read its byte[] source on pool threads while didChange edited the byte array, swapped the tree, and eager-close()`d the previous native tree on the dispatch thread; the fields were plain (non-volatile, non-synchronized). That raced a walk of a freed native tree (arena `IllegalStateException / use-after-free killing the request), a torn (source, tree, version) triple so Nodes.text extracted garbage and hover/diagnostic ranges were wrong, and worst a WorkspaceEdit computed against mismatched offsets that the client applies and corrupts the user’s schema file (completion could even tear against itself across an interleaved edit). Design (settled at Spec via the principles-architect consult): copy-on-read snapshots scoped by the workspace. New immutable FileSnapshot(tree, source, version) record carries a ts_tree_copy clone (jtreesitter’s documented cross-thread mechanism) whose native lifetime is independent of the live file’s, so the eager previous.close() and in-place tree.edit() on the dispatch thread stay exactly as they were and cannot invalidate a reader’s clone; WorkspaceFile.snapshot() captures the triple under the Workspace lock (the same lock the mutators run under, giving the happens-before edge, so no volatile on the plain fields). FileSnapshot is a deliberately distinct type from WorkspaceFile with no shared read interface, so the compiler (not convention) enforces "safe to read off the dispatch thread" and a call site cannot rebind the mutable instance. Workspace exposes lambda-scoped accessors, not the snapshot’s lifetime: withView(uri, absent, present) snapshots one file (or short-circuits to absent), withAllViews(present) snapshots every open file under one lock acquisition into an ordered Map<String, FileSnapshot> so a composed cross-document WorkspaceEdit is computed against one consistent generation of the whole workspace; both close every clone (including partials taken before the lambda throws) in a finally, making leak-by-omission structurally impossible (jtreesitter registers no Cleaner, so an unclosed clone leaks native memory until process exit) and keeping the native lifecycle in the imperative shell. The live-file handout Workspace.get and the now-orphaned openUris are removed from the public surface; WorkspaceFile no longer escapes Workspace. All reader call sites migrate: the five async handlers wrap their supplyAsync bodies, publishDiagnosticsForRecalculate snapshots per drained URI, CodeActions.compute and IntraSchemaDefinitions.compute use withAllViews for a consistent cross-document generation, and feature compute() signatures switch from WorkspaceFile to FileSnapshot (accessor names match, so mechanical); the completion self-tear is fixed since one snapshot now feeds its position, directive, and value reads. FileSnapshot deliberately omits declaredTypes() / dependsOnDeclarations() (consumed only by Workspace’s own under-lock mutators, never off-thread). Coverage: `FileSnapshotConcurrencyTest is the named enforcer (plain JUnit in graphitron-lsp; the generator’s four-tier taxonomy does not cover LSP concurrency and these need no catalog) ; a snapshot stays walkable and pre-edit-consistent (text + version) after applyEdit and replaceContent eager-close the original tree, close is independent in both directions, and withAllViews pins one generation across a concurrent didChange; the existing handler/feature tests are the behaviour oracle for the mechanical migration. Tests mint snapshots through a package-local WorkspaceFileTestSupport bridge to the package-private snapshot(). Alternatives rejected: an immutable WorkspaceFile behind a volatile (needs a home for the reused incremental-parse Parser and refcounting/Cleaner for old-tree lifetime, more churn same result), and serialising requests against edits per document (holds the lock across slow feature computation, kills request concurrency). No user-visible surface (no protocol/goal/directive change), so no user-doc draft. Establishes the safety precondition for R347 Slice 5’s didClose close() (which must land on top of this, where closing the live tree cannot invalidate any snapshot); test 1 is the mechanical enforcer that keeps this true regardless of commit order. Independent-session In Review → Done review; full graphitron-lsp suite green (468 tests, 0 failures) under -Plocal-db.

  • R452 (723266d, Spec c175917, Ready 91310e7): Reject explicit @reference and same-table participants on multi-table interface/union child fields, closing a silent-wrong-data hole. A single-cardinality multi-table polymorphic child field whose join path was an explicit @reference (condition or multi-hop) or whose participant shared the parent table built green yet returned an arbitrary participant row per parent: MultiTablePolymorphicEmitter.branchParentFkWhere returned null for any non-single-hop-FK shape and the caller lowered null to "no WHERE", so every parent resolved the field to the same wrong data with no error at any tier (the batched list sibling failed loud on a blind cast, proving the single arm was the odd one out). The fix is a classification-time gate plus a type lift, both landing at the single choke point FieldBuilder.resolveChildPolymorphicJoinPaths (all four producers: interface/union × table-backed/record-backed parent). Rule 1a rejects any field-level @reference structurally (a single stated path applies the same hops to every participant, so it is terminal-correct for at most one and cannot express a distinct join per participant; author-correctable by removing the directive); rule 1b rejects a same-table participant as a Deferred capability (participant table equals parent/hub → parsePath skips FK discovery → empty path → no correlation derivable; a self-FK participant is a legitimate schema, not an author error); rule 1c wraps zero/multi-FK auto-discovery failures with multi-table-child context so fkCountMessage’s generic "add a `@reference`" steer no longer leads straight into rule 1a. Both deferred sites point at the follow-up capability item (per-participant-multitable-child-join-paths, filed as R458). The type lift changes the per-participant carrier on `ChildField.InterfaceField/UnionField from a raw Map<String, List<JoinStep>> to Map<String, ParticipantFkPath>, a new record carrying the resolved single-hop FK column-pair slots with a non-empty invariant enforced at construction: the classifier decides "supported shape" exactly once and the emitter cannot represent an unsupported one, retiring branchParentFkWhere’s `instanceof/null-for-unsupported arm and batchedBranchJoinPredicate’s `(On.ColumnPairs) ((JoinStep.Hop) path.get(0)).on() blind cast (null survives only for the legitimate root-fetcher / participant-absent case). The single-hop-FK shape predicate is single-sourced (singleHopFkColumnPairs) across the multi-table arm and the single-table validateSingleHopFkJoin, and the stale stub-interface-union-fetchers.md pointer (R36 shipped) is dropped. Docs: polymorphic-types.adoc and multitableReference.adoc now state the rejection and point at the deferred capability. Coverage: new pipeline-tier MultiTableChildReferencePathRejectionPipelineTest (9 cases: condition/multi-hop/single-hop-{key:} @reference all reject identically by directive presence, union + record-backed-parent producer arms, same-table deferred rejection, zero-FK/multi-FK context wrappers, and the auto-discovered control carrying a non-empty ParticipantFkPath); TypeFetcherGeneratorTest fixtures migrated to the new carrier. Independent-session In Review → Done review; full reactor green under -Plocal-db -P!docs.

  • R453 (c529322, Spec e9cb78d): Reject sort-enum values that declare neither @order nor @index instead of silently skipping them. A partially-annotated sort enum bound to @orderBy built cleanly, contradicting the docs' promise of a per-value build failure (sort-results.adoc, order.adoc, orderBy.adoc all state a missing value "fails the build with a per-value diagnostic"); at runtime OrderByResolver continue`d past the unannotated value, and a request selecting only such values generated an empty ORDER BY, which on a paginated connection made keyset pagination slice a nondeterministic set (rows duplicate or vanish across pages). The rejection lands at the parse boundary in `OrderByResolver.resolveOrderByArgSpec: it accumulates every value lacking both directives while iterating the enum and, if the set is non-empty, returns a Resolved.Rejected after the loop, making the empty-ORDER-BY state unrepresentable in the model (namedOrders complete by construction). A new AuthorError.SortEnumMissingOrder(String enumTypeName, List<String> missingValues) arm carries the sort enum’s type name plus the full list of unannotated values (accumulate-all, not fail-fast, so the author sees every missing value at once; typed list rides for LSP fix-its rather than prose), following the RecordBindingMultiProducer shape for message() and prefixedWith. Per the Spec: the classifier’s anyMatch detection stays (detection vs completeness are distinct facts; a fully-unannotated enum never reaches the resolver and is already rejected as unclassified); no validator mirror (the parse-boundary rejection makes the bad state unrepresentable, and the model carries no full value list to re-derive); no generated-code guard (the resolver rejection is the single enforcer; emitted code carries no defensive guards for classifier-guaranteed shapes); and the annotated-value catalog-lookup fail-fast path is left untouched as a distinct failure class. No user-manual change (the docs already promise this exact failure; the change aligns code with the promise). Coverage: pipeline-tier GraphitronSchemaBuilderTest (partial annotation → build error naming the unannotated value; two unannotated values → single accumulate-all rejection listing both), plus drift guards RejectionSeverityCoverageTest (sample for the new arm) and SealedHierarchyDocCoverageTest (typed-rejection.adoc paragraph + drift-list mention); no generation-tier assertion (code-string assertions banned, the guard it would pin is dropped) and no execution-tier test (the nondeterministic state is now unbuildable). Sibling of R181 (validate-order-directive-args, the empty-@order / @order`@index+-coexistence gap); the arm is named so R181 can fold into a shared order-directive family later. Independent-session In Review → Done review; full reactor green under `-Plocal-db.

  • R449 (89746df, In Review dc70e62): Close the routine-chain classification edges surfaced by R435’s second-pass In Review review. Five design points, none gating R435’s shipped surface: (D1) gate the R435 root-chain interception on Query ; the interception read parentType instanceof RootType and routed root multi-node chains to classifyRootRoutineChain, so a @routine chain on Mutation landed a QueryRoutineTableField whose source() falsely asserts Root.Query; FieldBuilder.classifyField now reads the root position once (isRoot/isQueryRoot/isMutationRoot) and a Mutation @routine (multi-node in the interception, single-node from classifyMutationField’s top) lands a typed `Deferred signposting routine-mutation-write (R451’s write arm, a capability gap not an authoring error), while Subscription and non-routine Mutation chains fall through to their classifyRootField stories. (D2) fold @routine into both conflict detectors via a pairwise verdict table ; a sealed PairVerdict (Conflict | Deferred(planSlug) | Composes) that reduceDirectiveConflict projects over every unordered pair and reduces with Conflict-dominates-Deferred precedence, so @routine @lookupKey @service rejects the @service conflict rather than short-circuiting to the @routine×@lookupKey defer (the three-directive hole a slot-count carve-out would reintroduce); detectQueryFieldConflict is hoisted into classifyField before the interception and its old call inside classifyQueryField deleted (one detector site per position). (D4) repoint BuildContext.computeTerminalTargetVerdict’s `On.Lateral comment at FieldBuilder.routineChainVerdict (the SplitRowsMethodEmitter javadoc repairs stay R450’s scope). (D5) route the root routine fetcher (TypeFetcherGenerator) through the shared RoutineCallEmitter.emitCall via a new payload-free PreviousNodeRef.None arm and JoinPathEmitter.emitTableExpression, deleting the duplicated nonRoutineParamSource helper and inline ParamSource switch; QueryRoutineTableField’s compact constructor now pins every start binding to `ParamSource.Arg, making the None×SourceColumn arm genuinely classifier-unreachable (construction throws before any emit), and the consolidation is byte-identical (root correlated is false, so no DSL.val wrap). (D3) three text-only R435 rejection fixtures gain isInstanceOf arm assertions (DirectiveConflict for repeated @reference on ARGUMENT_DEFINITION; AuthorError.Structural for the input-field and element-less cases) plus a new fixture pinning the R300 single-node root desugar to hops = []. Coverage: 11 new pipeline-tier fixtures in GraphitronSchemaBuilderTest’s R435 block (D1 Mutation/Subscription landings, D2 conflict/defer/precedence verdicts across child + root single-node + root multi-node, the desugar pin); D4/D5 need no new tests (comments have no runtime surface; the consolidation is behaviour-identical under the existing R435 pipeline + execution suite, and the D5 constructor pin is exercised by every fixture that lands `QueryRoutineTableField); no code-string assertions on generated bodies. Independent-session In Review → Done review; full reactor green under -Plocal-db (503 GraphitronSchemaBuilderTest cases, 0 failures). Out of scope: the routine write arm itself (R451), R450’s SplitRowsMethodEmitter correlation rework, and lifting root position into a sealed RootType (a model-cleanup follow-up).

  • R446 (03dfe0b + c57fc57): Fix codegen crashing on array-typed database columns and reject array columns used as key elements. The R436 per-column TableRecord key reconstruction called ClassName.bestGuess(col.columnClass()) for every column in allColumns(), and for an array column jOOQ’s Field.getType().getName() is the JVM binary descriptor ([Ljava.lang.Boolean;), which bestGuess rejects, so any node type with an array-typed mapped column aborted graphitron:dev / generate with IllegalArgumentException: couldn't make a guess for [Ljava.lang.Boolean;. Fixed at the grain: decide each column’s Java type once at the jOOQ reflection boundary and carry it as a TypeName columnType on ColumnRef / JooqCatalog.ColumnEntry, decoded via TypeName.get(col.getType()) (array-safe, mirroring RoutineParam) at the three reflection sites and threaded through every construction site; the 31 ClassName.bestGuess(<ref>.columnClass()) codegen sites read <ref>.columnType(). The raw columnClass string is left in binary form intact for the consumers that depend on it (EnumMappingResolver Class.forName, SourceRowDirectiveResolver Class.getName() compares, GraphitronSchemaValidator), so this is a dual-fact split, not a replacement. ClassName-typed locals and DomainReturnType.Plain widen to TypeName (safe: TypeName.equals/hashCode are toString()-based and a scalar column still decodes to a ClassName, so R204/R279 multi-producer agreement is unchanged for scalars and distinguishes arrays). A validate-time rejection is added for an array-typed column used as a @node NodeId key column (validateNodeType) or a DataLoader @splitQuery batch key (validateField over BatchKeyField.sourceKey().columns()), because Java arrays compare by reference identity and would silently mis-batch / mis-match at runtime; merely "not throwing" there would turn a build-time crash into a silent correctness bug. A test-only 3-/4-arg auxiliary constructor derives columnType from a source-form columnClass (shared scalar-only decode, null on placeholders, array columns can’t reach it) so ~161 hand-built fixtures stay unchanged; ColumnTypeConstructorArityGuardTest scans the main-source tree and fails the build if any production construction regresses to the string-decoding form. Coverage: unit-tier ArrayColumnTypeDecodeTest (boundary decode: ClassName for scalars, ArrayTypeName of the right element for boolean[]/text[], columnClass stays the binary descriptor), pipeline-tier ArrayColumnCodegenPipelineTest (regression pin over a new array_holder fixture through the TableRecord key-extraction path, verified to reproduce the crash when reverted), and ArrayKeyColumnRejectionValidationTest (@node array-key rejection + scalar control); no code-string assertions on generated bodies. Compilation/execution tiers not added via sakila-example (array columns aren’t surfaced there; emitted-form validity is pinned by the boundary-decode toString()), and the DataLoader batch-key rejection arm has no fixture (unconstructable: PostgreSQL disallows an array FK target). Bug fix, regression from R436; no user-facing surface. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R435 (impl c0620e5..7a58239, In Review e288670): Order-significant @routine / @reference composition ; a jOOQ table-valued function is a table node (R333’s tableExpr RoutineCall) that can be a field’s row source, its projected terminus, or sit between tables in the join chain. Both directives become repeatable and their written order defines the table chain (implicit head → contributions → terminus, last node == the field’s @table); ReferenceElement gains no arm and @reference no new luggage, so composition is directive co-occurrence plus order, not a widened input (the rejected-alternatives log records why the @oneOf element arm, a slim path directive, @reference(from:), and a fixed-orientation rule were all turned down). Discharges R333’s deferred SDL-surface residue and its root-entry-validator residue (b). Model: on R438’s two-axis Hop(TableExpr target, On on) substrate, adds TableExpr.RoutineCall, the positive On.Lateral arm, On.Keying (ForeignKey | NameMatchedKey, the FK-less name-matched key for hops adjacent to a routine result), ParentCorrelation.OnLateralArgs, and a ParamSource.SourceColumn arm so a routine IN parameter has exactly one source shape (argMapping → Arg, columnMapping → SourceColumn, the correlated CROSS JOIN LATERAL case); every pre-existing sealed switch on On / ParentCorrelation gained an explicit arm and the root QueryRoutineTableField was re-homed onto the (start, hops) chain (R300 desugars to hops = []). Shipped end to end at root and child positions: correlated single-node (lateral), routine-then-hops, hops-then-routine, sandwich, and repeated-@reference composed chains, through one chain walker shared by root and child classifiers; inline correlated multiset and the @splitQuery batched keyed re-query on table-backed parents (batch key = the routine’s column-bound inputs). Typed classify-time rejections (root-head, terminus, columnMapping existence/type/one-source, repeated @reference off FIELD_DEFINITION, Connection-terminus, uncorrelated-@splitQuery) each carry a validator projection and a fixture; @orderBy / @condition / catalog-terminus-pagination / @lookupKey land typed Deferred. Coverage: pipeline fixtures per chain shape in GraphitronSchemaBuilderTest’s R435 block, execution-tier proofs on a `films_for_actor fixture function (RoutineFieldExecutionTest, per-parent correlation, mixed column/argument binding, batched-vs-inline row equivalence, hops projecting a film-only column so a mis-keyed hop cannot pass), and the full rejection-fixture set; no code-string assertions. routine.adoc / reference.adoc rewritten for the composition surface and order contract. Remaining fetch-form breadth (multi-routine chains, record-backed / TableInterfaceType parents, @lookupKey) re-homed to R447; ordering/binding/corpus residue to R448. Independent-session In Review → Done review; full reactor green under -Plocal-db (3802 tests). Builds on R333/R438; supersedes R300’s root-only @routine.

  • R445 (579ec14, In Review 39ecbb9): Resolve a participant cross-table @reference column read by the FK-pinned terminal TableRef (class identity), not a bare SQL name re-resolved through the catalog. Seventh audited site of the schema-qualified @table bug class (siblings R396, R440, R441, R442, R422, R444), found by R444’s spec-time audit. TypeBuilder.extractCrossTableFields held the FK-pinned fk.targetTable() (identity-carrying since R441) and the resolved interfaceTable, but re-resolved their bare tableName() strings through JooqCatalog’s string lookups, which return `TableResolution.Ambiguous when the FK terminal’s bare name collides across generated schemas: the column resolve came back empty and the field was silently skipped from the participant’s cross-table set, falling through to FieldBuilder’s scalar `@reference path where (post-R444) it misclassified as a plain ColumnReferenceField instead of ParticipantColumnReferenceField, so the interface fetcher emitted no conditional LEFT JOIN / alias projection and a participant field’s classification came to depend on whether an unrelated schema happened to hold a same-named table. No author-side workaround: the FK terminal is not author-named (the @reference key is TABLE__CONSTRAINT on the source table), and this path never routes through ServiceCatalog.resolveColumnForReference, so R444’s overload retirement could not catch it. Design (same as R440/R441/R422/R444, "decide once, carry the decision as a type"): consume the carried refs directly via R444’s TableRef.column(String) matcher and TableRef.allColumns(), retiring all four string-keyed catalog reads from the method ; R388 defect-2 guard predicate + base column set → interfaceTable.column / allColumns; detail-only candidate hint → fk.targetTable().allColumns(); column resolve → fk.targetTable().column(columnSqlName), dropping the manual ColumnEntry → ColumnRef conversion. extractCrossTableFields now holds zero string-keyed catalog reads, one resolution story instead of two. Behavioral deltas: a colliding FK terminal now yields a ParticipantColumnReferenceField (the fix), and the R388 rejection’s candidate hint is non-empty when the detail table collides; unknown columns still skip to the field-level classifier and the guard still fires on base-resident columns. Coverage: pipeline-tier QualifiedParticipantCrossTableReferencePipelineTest (sibling of R444’s QualifiedTerminalReferenceColumnPipelineTest and R422’s QualifiedReturnTypeReferencePipelineTest, over the existing multischema fixture, no new DDL) ; a cross-schema-colliding FK terminal classifies green as ParticipantColumnReferenceField with the resolved column asserted (not vacuous); a column present only on the other schema’s same-named table still rejects (schema-pinned, not search-all-schemas), its diagnostic naming the FK-pinned A-side candidates; a base-resident column trips the R388 contradiction guard with a now-non-empty detail-only candidate hint. Out of scope by design: ctx.parsePath’s path-start `tableName() echo (author-named and qualifiable per R396). Independent-session In Review → Done review; red-before-green verified (tests 1 and 3 fail with the fix reverted); full graphitron suite green (2489 tests) under -Plocal-db. Closes the audited no-workaround FK-terminal @reference sub-class (R444 scalar + R445 participant cross-table); does not claim the whole schema-qualified @table class closed ; the author-qualifiable bare-name reads (ServiceCatalog.resolveColumn, path-start echoes) remain by design.

  • R444 (e5f3944, In Review 3785229): Resolve a scalar @reference terminal column read by the FK-pinned terminal TableRef (class identity), not a bare SQL name re-resolved through the catalog. This was the sixth site of the schema-qualified @table bug class (siblings R396, R440, R441, R442, R422): despite R422’s changelog claiming the class closed, R422 only fixed the object-return-type terminal verdict; the scalar @reference column read was a separate unaudited path with the identical defect. ServiceCatalog.terminalTableSqlName walked the FK path and returned hop.targetTable().tableName(), collapsing the identity-resolved terminal TableRef (which R441 populated with a tableClass identity) to a bare name string; resolveColumnInTable then re-resolved it through JooqCatalog.findColumn(String, …), which hit TableResolution.Ambiguous when the terminal table name collides across generated schemas and demoted the field to UnclassifiedField with a spurious "column could not be resolved" author error and no author-side workaround (the @reference key names the FK on the source table, so there is no syntax to qualify the FK terminal). Design (same as R440/R441/R422, "decide once, carry the decision as a type"): replace terminalTableSqlName / terminalTableSqlNameForReference with the ref-carrying terminalTableForReference(List<JoinStep>, TableRef) (walk the path, terminal is the last hop’s targetTable(), empty path yields start, condition-only steps still bail to empty); retype resolveColumnForReference to take the start as TableRef and resolve the column off the terminal ref via new TableRef.column(String) ; the single model-side matcher home mirroring findColumn’s order (javaName` equalsIgnoreCase across all columns, then sqlName), never a bare-name catalog re-resolve. The scalar-output unknown-column diagnostic now enumerates the terminal TableRef.allColumns() java names (previously empty on a colliding terminal, ambiguity-broken too); the argument-filter (FieldBuilder) and input-field (BuildContext) sites pass their already-resolved TableRef. Behavioral deltas: a colliding terminal now resolves; condition-only paths, unknown columns, and empty paths keep today’s outcomes. Coverage: pipeline-tier QualifiedTerminalReferenceColumnPipelineTest (sibling of R422’s QualifiedReturnTypeReferencePipelineTest, over the existing multischema fixture, no new DDL) ; a @reference read on the cross-schema-colliding FK terminal classifies green as ChildField.ColumnReferenceField (with the resolved column asserted, not vacuous); a column present only on the other schema’s same-named table still rejects (schema-pinned, not search-all-schemas); a genuine unknown column rejects with a non-empty candidate list. Two adjacent bare-name reads stay out of scope by design: the direct non-@reference scalar read (source @table echo resolves qualified, author has a workaround) and the participant cross-table @reference path (a genuine seventh site, no workaround, tracked as R445, which consumes this item’s TableRef.column matcher). Independent-session In Review → Done review; full graphitron suite green (2526 tests) under -Plocal-db. Closes the FK-terminal @reference column-read sub-class (scalar output field, argument filter, input field); does not close the whole bug class (the participant cross-table path is R445).

  • R422 (6955bf6, In Review a1edbe0): Compare return-type identity in the @reference terminal-target verdict, not the verbatim @table echo. BuildContext.computeTerminalTargetVerdict decided whether an @reference path’s terminal hop lands on the field’s return-type table via TableRef.sameTable (a bare equalsIgnoreCase against the return type’s verbatim @table string), so a schema-qualified return @table (e.g. multischema_a.widget while the hop resolves to jOOQ’s unqualified canonical widget) spuriously reported Mismatch and demoted the field to UnclassifiedField. Last open member of the schema-qualified @table bug class after R396 (source-side FK predicate), R440 (FK-join endpoint/FK identity), R441 (landed TableRef.denotesSameTableAs), R442 (condition-param match). Chosen design (settled at Spec against a full parsePath String→identity migration): thread the already-resolved return-type TableRef into parsePath as its own nullable axis alongside the existing targetSqlTableName String ; name stays the input to the name-based plumbing (empty-path FK inference, condition-join terminal build), the ref is consumed only by the verdict, which now compares via R441’s TableRef.denotesSameTableAs (both sides catalog-constructed, so identity-vs-identity). The seven non-null-target call sites pass the ref they already hold one frame up (six in FieldBuilder, plus NodeIdLeafResolver which hoists its findTable above resolveFkJoinPath so ref and name pass together); the return-side null gate moves to returnTableRef == null and the Mismatch message keeps rendering the author’s verbatim echo. No R440-style resolve-or-fall-back contract (an unresolvable return name is unreachable here and re-resolving could reintroduce the bug on a bare cross-schema collision). Coverage: pipeline-tier QualifiedReturnTypeReferencePipelineTest (sibling of R396’s QualifiedSourceReferencePipelineTest over the multischema jOOQ fixture, no new DDL) ; the schema-qualified return @table classifies green as ChildField.TableField and the terminal hop genuinely lands on widget, paired with a genuine mismatch (return type bound to event, hop lands on widget) that still rejects to UnclassifiedField; no code-string assertions. TableRef.denotesSameTableAs already pinned by R441’s TableRefSameTablePredicateTest, no new predicate coverage. Independent-session In Review → Done review; full reactor green under -Plocal-db. Builds on R396/R441; closes the schema-qualified @table bug class.

  • R440 (d0bd8ca, Spec 6cfda86/426c586): Resolve FK-join synthesis endpoints and the FK itself by jOOQ class / reference identity, not bare SQL name. BuildContext.synthesizeFkJoin already held the jOOQ ForeignKey object (whose endpoint Table classes it pins exactly) but re-looked-up both endpoints and the FK by bare name, reintroducing the ambiguity R396 removed: two schemas sharing a bare table name yielded TableResolution.Ambiguous and the join failed, and a constraint name colliding across schemas silently returned the first-hit FK (a wrong-join hazard, not a rejection). Another member of the schema-qualified @table bug class (R396 done; R441/R442 the accessor and condition-param siblings landed just prior; R422 Backlog). Four moves, all "decide once, carry the decision as a type": (D1) new JooqCatalog.findForeignKeyRef(ForeignKey) resolves the Keys-class constant by reference identity in the FK-holder schema only (the FK-child endpoint class structurally pins the owning schema), the FK-singleton invariant its named enforcer; fkJavaConstantName retargeted onto the FK object; the false-docstring findForeignKeyByName deleted. (D2) synthesizeFkJoin resolves both endpoints via findTableByClass off the FK, so UnknownTable becomes a defensive-only arm (fires on catalog-vs-FK mismatch, never bare-name ambiguity); the fabricated-source test case retired in favour of upstream membership checks. (D3) the Optional-returning findForeignKey(String) (which could only collapse a collision into "not found") replaced by a scoped, sealed findForeignKey(name, sourceSqlName) returning ForeignKeyLookup (Resolved/NotInCatalog/Ambiguous); the three author-facing sites ({key:} path element, IdReference synthesis shim, explicit @reference(key:) record-FK) reject Ambiguous through a new ambiguousForeignKeyRejection structural builder naming the colliding schemas + qualified forms, while the non-author-facing qualifierForFk migrates keeping its Optional contract. (D4) findUniqueFkToTable returns Optional<ForeignKey> and NodeIdLeafResolver consumes the object directly, dropping the name round-trip that re-collided after findForeignKeysBetweenTables had already resolved by class. Fixture: a note table in both multischema schemas, each with an FK named note_event_fk into its own schema’s event, giving both a colliding bare target-table name and a colliding FK constraint name, plus seed rows for execution-tier reuse. Coverage: unit-tier JooqCatalogMultiSchemaTest D1-D4 cases (per-schema Keys-class ref resolution, endpoint-by-class from each side, scoped disambiguation, null-scope Ambiguous naming both schemas, structural-prose rejection) plus the {key:} membership enforcer parsePathElement_keyNotTouchingSource_rejectsBeforeSynthesis; findUniqueFkToTable directionality cases migrated to the FK-object shape in JooqCatalogIdRefTest; no code-string assertions on generated bodies. Resolver-tier wiring judged disproportionate (nodeid fixtures stay single-schema; the collision lives at the catalog/BuildContext tier), recorded in the item. ForeignKeyLookup is a JooqCatalog-local result type in the TableResolution/ForeignKeyResolution/RoutineResolution family, out of scope for VariantCoverageTest/SealedHierarchyDocCoverageTest; the ambiguous arm produces an existing Rejection.structural leaf, adding no typed-rejection.adoc obligation. Independent-session In Review → Done review; full reactor green under -Plocal-db. Builds on R396/R438; sibling of R441/R442/R422.

  • R442 (87d25a9, Spec b9c8cfa): Make the R379 Check-2 concrete-condition-param table match compare by jOOQ class identity, not a bare-vs-qualified name string. BuildContext.checkConcreteParamTable compared the parameter’s bare jOOQ table name (event) against the hop’s possibly schema-qualified @table echo (multischema_a.event) with equalsIgnoreCase, so in a multi-schema catalog a concrete @condition parameter typed with the correct generated table class was false-rejected (author’s only workaround: widen to Table<?>, discarding the type safety), and two same-named tables in different schemas were indistinguishable. Another member of the schema-qualified @table bug class (R396 done, R441 the accessor-side sibling landed just prior, R422/R440 Backlog); surfaced from gap D of the opptak multi-schema migration. Design shape 1 (chosen over re-resolving the string inside the check): thread the resolved TableRef`s down the validator chain instead of name strings. `validateConditionParamTables / checkConcreteParamTable now take TableRef source/target; the condition-hop site threads the hoisted conditionOrigin (null when the source is not table-backed, the existing skip) and r.target(), the where-filter site threads hop.originTable() / hop.targetTable(). The final compare is TableRef.denotesSameTableAs (the shared identity-body-with-name-fallback predicate R441 landed first and this item reused verbatim per the coordination note, dropping the predicate + consumer-audit scope), so a parameter typed with the right class classifies green even against a qualified echo and cross-schema collisions stay distinct; the mismatch message renders the declared side schema-qualified so a bare-name collision stays actionable. Coverage: pipeline-tier MultiSchemaConditionParamTest + MultiSchemaConditionStub over the colliding-event multischema fixture, covering source (terminal condition + where-filter) and target (terminal condition) operands, both green and by-identity wrong-schema rejection; the catalog-built wrong-schema cases double as the fallback-arm enforcer. Additive multischema_a.event_log DDL (jOOQ schema version bumped 2.4→2.5) backs the where-filter source shape. Test-plan deviation recorded in the item + landing commit: target-side coverage rides a terminal condition hop rather than the spec’s where-filter hop, because the where-filter FK-endpoint resolves event by bare name through synthesizeFkJoin (ambiguous across the two schemas), which is R440’s scope, not R442’s. Independent-session In Review → Done review; full graphitron suite green under -Plocal-db (2471 tests). Builds on R379; reuses R441’s predicate.

Historical record of completed rewrite work. Entries are roughly reverse-chronological; commit shas and plan slugs are preserved for archaeology. The forward-looking ledger lives in README.md, generated from per-item front-matter in this directory.

The next-id: front-matter field is the canonical counter for R<n> allocation, maintained by roadmap-tool create. Numbers are never reused (see workflow.adoc); the counter advances past every Done so the gaps left by deleted item files don’t collide with future allocations.


  • R441 (e0b878a implementation; Backlog → Spec 62d1222, Spec → Ready d5117fa, In Progress 9d3756c, In Review 2fdb708; independent-session In Review → Done review): Typed-accessor match on a free-form DTO payload parent now compares reified jOOQ table-class identity, not the bare @table name. Same multischema migration as R396/R422’s family (gap E of graphitron-qualified-names-gaps): once an element type’s @table is schema-qualified to disambiguate a cross-schema bare-name collision (multischema_a.event vs multischema_b.event), the verbatim @table echo never equalsIgnoreCase’d jOOQ’s always-unqualified canonical `event, so FieldBuilder.collectAccessorMatches silently dropped the accessor and the payload parent rejected with RecordTableField … requires a typed accessor or @sourceRow plus cascading WrapperArm errors transport failures on the sibling errors field. Fix routes the comparison through the identity already in hand: TableRef.denotesSameTableAs(TableRef) compares the carried tableClass (ClassName, structural equals) when both refs have one, falling back to the case-insensitive name compare only for fixture-built classless refs (catalog-constructed refs via JooqCatalog.TableEntry.toTableRef always populate it, so production always takes the identity arm); collectAccessorMatches threads the expected TableRef instead of its bare SQL name and filters via denotesSameTableAs, with the accessor side already resolved by record-class identity (ServiceCatalog.resolveTableByRecordClass) so the compare is identity-vs-identity. The predicate’s javadoc names the two identity homes (parse-boundary raw-Table<?> primitives on JooqCatalog per R396 vs. model-side reified ClassName here) so a future consumer picks by where it stands rather than growing a third mechanism. Upgrading the predicate body silently switched the four other consumers (TypeBuilder ×3, GraphitronSchemaValidator, FieldBuilder hop-origin) to identity comparison; each compares two same-catalog-derived refs, so any change of verdict is a cross-schema false-positive becoming correctly false (strictly tightening). Gap D stayed out of scope, filed as R442 which subsequently adopted the same predicate. Coverage: unit-tier TableRefSameTablePredicateTest pins all three arms (same class/divergent names → true; same bare name/different class → false, the silent-regression guard; classless fallback); pipeline-tier TypedAccessorSchemaQualifiedIdentityPipelineTest over the multischema fixture pins both directions (qualified echo classifies green as RecordTableField with accessor-derived source and schema-A tableClass; different-schema record dropped and field rejects), asserting classifier outcomes and sourceKey, no code-string assertions on generated bodies. TableNameComparisonCaseGuardTest untouched (identity route lives in the guard’s excluded PREDICATE_HOME). Full reactor green under -Plocal-db.

  • R438 (materialize-joinpath-facts; slices 3754f40 JoinConditionRef, 8106991 axes minted, 099aa30 producer/reader cutover, d3dafa3 flat-variant delete, b0ab513 self-review fixes; independent-session In Review → Done review): JoinStep reshaped onto R333’s two orthogonal axes ; Hop(TableExpr target, On on, originTable, JoinConditionRef filter, alias) with On.ColumnPairs | On.Predicate and the day-one TableExpr.Catalog arm; the flat FkJoin / ConditionJoin variants and the WithTarget capability deleted, slot iteration now the standalone HasSlots capability shared by On.ColumnPairs and the transitional LiftedHop (both retired by R431). Absorbed and closed R16. Generated output byte-identical per slice; gate review re-verified the full reactor green under -Plocal-db and fixed one stale WithTarget mention in emitter-conventions.adoc.

  • R16 (fkjoin-model-cleanup; absorbed and closed by R438’s slice 1 + cutover): the join-condition calling convention is now typed. JoinConditionRef wraps the MethodRef population called as method(srcAlias, tgtAlias) by JoinPathEmitter.emitTwoArgMethodCall, which takes the wrapper directly so call sites stop extracting raw MethodRef`s; handing a `WhereFilter-convention method to a join-condition emit site is a compile error. R16’s whereFilter naming complaint dissolved structurally in the R438 cutover: the ON-clause condition and the WHERE-appended filter became differently-named components (On.Predicate.condition vs JoinStep.Hop.filter) instead of one overloaded whereFilter.

  • R439 (d34ef37 implementation; filed 66dea39, Backlog → In Review ea67d6c at the user’s direction with the implementation pre-landed and validated in the originating session, the Spec/Ready flips recorded as mechanical passthrough; independent-session In Review → Done review): Background dev-environment warm-up for Claude Code Web sessions. The SessionStart hook (.claude/scripts/session-start-web-env.sh) now runs asynchronously in web sessions (CLAUDE_CODE_REMOTE=true): it emits {"async": true} so the session starts immediately, establishes the prerequisites (JDK 25 + alternatives/profile retarget, PostgreSQL rewrite_test drop/reseed, Maven settings de-proxy, libtree-sitter 0.26.9) in the background, then warms the whole reactor with mvn -B -ntp install -P 'local-db,!docs' -DskipTests, tracking state in /tmp/graphitron-web-env.status (prereqs/warm-build/done/failed + hook PID + epoch, with an EXIT/TERM/INT trap so a killed hook leaves failed, never a stuck running state) and logging to /tmp/graphitron-web-env.log. A new PreToolUse Bash guard (.claude/scripts/wait-for-web-env.sh, registered in .claude/settings.json with a 2700s timeout) extracts the command from the hook JSON and holds mvn commands through both phases and psql commands through the prereqs window, so a foreground build can never race the background one into the catalog-jar clobber; it fails open on a dead hook PID, a stale (>2400s) status, or an unparseable status file, and is also runnable by hand as a wait-with-log-tail. The JDK step additionally persists JAVA_HOME through $CLAUDE_ENV_FILE so agent shells stop inheriting a stale JAVA_HOME=java-21 past the enforcer. Local sessions keep the fully synchronous behavior and never create the status file. Dev-tooling only, no generator code affected; gate review re-verified the guard’s hold/release/fail-open matrix against synthetic status files and observed the live warm-up (async start, warm-build → done, BUILD SUCCESS, JDK 25 in the agent shell) in the reviewing session’s own sandbox.

  • R434 (3b38beb implementation; Spec → Ready ee39933, In Progress → In Review 3cac680; independent-session In Review → Done review): Restructured the rewrite design principles doc around axioms with named enforcement. rewrite-design-principles.adoc (28 flat peer sections, the type-system family stated five times, the central R222 thesis living only in a preamble pointer) is replaced by docs/architecture/principles/development-principles.adoc: six axioms (decide once at the parse boundary / orthogonal facts are independent axes / one model many views / boundaries decode and encode / every invariant has an enforcer / generated code is a consumer artifact), each principle carrying rule + exemplar + smell + an *Enforced by:* line naming what fails when it breaks (compiler / named meta-test / build tier / the honest gap label "review only", which doubles as the meta-test gap list). The ingress states the FCIS + normalization spine and coins the drift smell once (R268 narrated solely under the enforcement axiom; field/model/format instances cite it). The Emitter Conventions catalogue extracted to docs/architecture/reference/emitter-conventions.adoc (plus a new cursor encode/decode section); the dunder-rule full semantics moved to GeneratedSourcesLintTest’s javadoc next to their enforcer; additive-then-cutover change discipline relocated to `roadmap/workflow.adoc. The doc budgets itself at 3,500 words (landed at 3,456), enforced by new DocSizeBudgetTest.developmentPrinciplesStaysUnderBudget. Citation sweep retargeted every live reference (docs xrefs with changed section anchors, javadoc, .claude prompts, CLAUDE.md, roadmap-tool boilerplate, live roadmap items); historical records keep the old name. Docs-only plus the budget meta-test; full reactor green under -Plocal-db. Builds on R433.

  • R433 (420cd32 implementation, d905fac rework 1, 2ba655a rework 2; Ready sign-off under the user’s in-session short-circuit, In Review → Ready gate 1 ad18fa5, In Review → Ready gate 2 aa6fe29): docs/architecture/explanation/rewrite-design-principles.adoc violated its own "Documentation names only live tests/code" rule by carrying unguarded live-inventory enumerations (arm lists, file censuses, occurrence counts, a dated compliance roster) that rot silently as the codebase moves, and canonized surfaces the R222/R333 pivot dissolves. New "Principles are stated at altitude" section codifies the discriminator: an inventory belongs in a principle only when a named live test pins it (the GeneratorCoverageTest-guarded dispatch partition is the kept exemplar); otherwise state the rule, one canonical exemplar, and the smell. Applied across SourceKey.Reader/Wrap arm enumerations, the four-axis SourceKey/LoaderRegistration census, two parse-boundary file censuses, the CallSiteExtraction strategy list, the candidateHint occurrence census, the helper-locality compliance roster, and the R50 retired-carrier roster; vision-alignment forward notes added pointing SourceKey at R431 and MethodBackedField at its R222 retirement (ServiceField as the current capability exemplar); stale ArgCallEmitter.buildNodeIdDecodeExtraction/R260 citation replaced (R260 shipped; R334 tracks the live instance). Two review rounds found and fixed residual rot the first pass missed: gate 1 caught a still-enumerated retired TextMapLookup permit (R229), an 11-of-12 module census, an unguarded "thirteen resolver siblings" count, and an over-inclusive java.lang.reflect-import discovery recipe (keyed onto Type-tree reads instead), plus same-family ride-alongs in code-generation-triggers.adoc, argument-resolution.adoc, typed-rejection.adoc, SealedHierarchyDocCoverageTest, and SourceKey.Reader’s javadoc; gate 2 caught a sentence-initial "Thirteen directive resolvers" in `typed-rejection.adoc that survived gate 1’s case-sensitive grep sweep, fixed with a case-insensitive re-sweep (zero remaining hits outside an unrelated PGThirteen test fixture). This is a docs-only item: no generated-output or runtime change; verification is the stale-reference check on every symbol/test/roadmap-id the revised doc names, plus a clean AsciiDoctor render. Full reactor green under -Plocal-db at each gate.

  • R436 (2992d25 implementation; Backlog → Spec 20b5072, Spec → Ready 97c0bb3, In Progress → In Review e263020): Fix unsafe into() key extraction colliding with multiset aliases and escaping error redaction. Two defects on a @service/@splitQuery split field over a @table parent whose DataLoader key wrap is SourceKey.Wrap.TableRecord. Defect 1: the key read did a whole-record env.getSource().into(Tables.X), mapping the parent row into the typed record by column name; a sibling multiset-backed object field aliased (.as(fieldName)) to a name case-insensitively shadowing a physical column (the incident: dager/tider over range columns; the sakila repro: Film.Length over FILM.LENGTH smallint) poisoned the conversion and threw a MappingException, aggravated by R426 widening the parent SELECT to the full row. Defect 2: that throw ran synchronously in the DataFetcher body before dispatch and the async .exceptionally router, so it escaped DataFetcher.get() unrouted and leaked jOOQ’s raw record-dumping message past ErrorRouter redaction (a privacy hole; per-node repetition blew the OTel gRPC 4 MiB export limit). Fix (Defect 1): the parent $fields projects the full row under reserved __src_<col>__ aliases (GraphQL reserves leading-__ for introspection, so no client-driven sibling alias can collide) and GeneratorUtils.buildKeyExtraction’s `TableRecord arm rebuilds the typed record column by column with explicit types (no runtime Field<?> loop / unchecked cast). Both emit sites drive off a new generation-time TableRef.allColumns (populated in JooqCatalog), single-homing the projected names and the extraction’s lookup names so they cannot drift. RequiredProjection reshaped from a sealed { FullParentRow | Columns } sum to a product record (boolean reservedFullRow, List<ColumnRef> baseColumns) ; the reserved full row no longer supplies base-named columns that the Wrap.Row/Wrap.Record/TableMethodField reads still need, so the two axes are co-present and both emitted (R426’s absorbing "type fact" javadoc rewritten). A narrow build-time validator (GraphitronSchemaValidator.validateAliasKeyColumnCollisions + parentProjectionAlias, mirroring emitSelectionSwitch) rejects a sibling alias shadowing a key/correlation column read by base name (the residual the reserved-alias fix cannot cover), with field/column/remedy in the message; the broad whole-row collision is fixed not rejected, so legitimate schemas keep working. Fix (Defect 2): DataLoaderFetcherEmitter.build wraps extraction + dispatch + async tail in try/catch (Throwable) routing through the same asyncRouterCall disposition the .exceptionally arm uses (single-homed in TypeFetcherGenerator, threaded to both arms so they cannot diverge), lifted into a completed future; the R268 preRegistrationPrelude stays outside the guard by design. Coverage: execution-tier GraphQLQueryTest.films_titleTitlecase_withCollidingMultisetSibling_bothResolve_noMappingException (both fields resolve post-fix), pipeline-tier ServiceProjectionPipelineTest two-axis emit + AliasKeyColumnCollisionValidationTest (rejection + no-false-positive), unit-tier DataLoaderFetcherEmitterTest guard-shape (registration outside / extraction inside / catch-arm routes), R426 contract tests still green; no code-string assertions on generated bodies. Reserved __src_* reaches generated code only as string literals, so the dunder lints need no allowlist entry (docs updated); handle-services.adoc extraction phrasing updated. Spec-permitted fallback taken on the execution-tier redaction test (no bespoke throwing-accessor fixture): the sync catch is unit-proven, shares one router-call definition with the async arm, and the async arm’s redaction is already execution-proven via Film.durabilityError. Independent-session In Review → Done review; full reactor green under -Plocal-db (all 13 modules incl. the graphitron-sakila-example execution + Java-17 compile tier). Builds on R426/R425/R415/R268.

  • R437 (f293803 implementation; Backlog → Spec 4119cf7, Spec → Ready d8c8653, Ready → In Progress 0fef51c, In Progress → In Review e41a4d8): Shape-aware create<Record> / create<Record>List @service helper dedup, fixing an R311/R315 silent-column-drop correctness bug. The jOOQ-TableRecord @service param helpers were deduplicated by record class alone (putIfAbsent(recordClass, …) at two collection sites, and both call-site namers derived from recordClass.simpleName()), so two @service fields on one type binding the same record through different input shapes (different @field column sets) collapsed to the first-seen helper; every call site routed to that survivor and the other mutation silently wrote its unique columns as NULL/default (found in fs-plattform’s `registrerCampusForUtdanningsmulighet vs deaktivereCampusForUtdanningsmulighet, where DATO_FRA was dropped to 1900-01-01). The fix re-keys dedup, naming, and call-site routing by the full binding shape (record class + ordered ColumnBinding`s + ordered `RecordKeyDecode`s): a new `JooqRecordHelperNames resolver dedups on the carrier’s own structural equals (D1 ; exactly "these two emit an identical helper body", so no parallel signature function can drift), names uncontended record classes with the bare create<Record> (byte-identical to pre-R437, no churn) and contended ones with canonicalRender-ordered 1-based ordinal suffixes plus a one-line column-naming javadoc (D2), and is built once from every jOOQ-record carrier on the <Type>Fetchers class (both coordinates) and stashed on TypeFetcherEmissionContext before any field body emits, so the helper drain and both call-site emitters (ArgCallEmitter child, ServiceMethodCallEmitter root incl. the list arm) resolve the same name by construction (D3); a populated resolver throws on an uncollected carrier rather than silently falling back to a bare name (routing-hole tripwire), while the default bare() resolver preserves today’s behaviour for schema-free/unit/out-of-band contexts. Accepted limitation: collapse compares bindings in producer order, so identical columns in different SDL declaration order would not collapse (a missed collapse, not a correctness bug). Coverage: pipeline-tier JooqRecordServiceParamPipelineTest R437 group ; contended-singular red regression (two distinct helpers, each fetcher routes to its own, exactly one sets RELEASE_YEAR), contended-list arm pinned separately, cross-input-type collapse to one bare helper (pins shape-keying over input-type-name keying), determinism across runs, and contended-vs-uncontended javadoc; the R311/R315/R322/R336 pins keep passing (36/36). Independent-session In Review → Done review; full reactor green under -Plocal-db incl. the graphitron-sakila-example Java-17 compile (no generated-output churn). Builds on R311/R315.

  • R410 (e7ae955 slice 1, 1fae7e9 slice 2, 55e04e3 slice 3, 6149cfe slice 4, a0e6950 slice 5, d77fd0c slice 6; review fixes in the In Review → Done commit range): graphitron:dev owns incremental compilation of generated sources. The dev loop now turns generated .java into .class in-process, into the graphitron-exclusive target/graphitron-classes (sole-writer dir, first-on-classpath precedence for consumers that load external .class; quarkus:dev empirically confirmed not to consume it, so the Quarkus value routes to the in-process MCP query-execution driver, filed as R428/R429). Composition: the idempotent writer reports its per-run delta (slice 1); CompileDependencyGraph is projected from the classified model through exhaustive switches over the GraphitronType/GraphitronField leaves mirroring projectFieldClassification, with frozen-vs-growing UtilSingleton classification so blanket over-approximation never harms pruning (slice 2); AbiSignature hashes the signature surface (constant values included for javac inlining; type variables with bounds added in review) and RecompileSet computes delta ∪ ABI-changed reverse-transitive dependents as pure functions (slice 3); a warm JavaCompiler/StandardJavaFileManager engine with per-round fresh tasks, orphan .class sweep, and a dedicated CompileDiagnostic channel kept separate from ValidationReport (slice 4); the two-clause acceptance harness (incremental tree byte-for-byte equals clean full compile; body-edit prunes / ABI-edit propagates) plus the TypeSpecReferenceWalk completeness oracle (slice 5); DevMojo wiring with -Dgraphitron.dev.compile=false opt-out, no fail-fast (exclusive dir degrades safely), conservative whole-tree recompile on consumer .class change, and compile diagnostics surfacing through the console block (CompileErrorFormatter) and the MCP diagnostics tool with a source: "schema"|"compile" discriminator (slice 6). Independent-session In Review → Done review landed four fixes: the completeness oracle widened with a @node type, which falsified missing node-lookup wiring edges (QueryNodeFetcher/EntityFetcherDispatch including the per-node-type types.<T> projection references and entitiesByType federation targets) ; closed model-sourced in the builder; failed compile rounds now carry a retry set so an unrelated save can never report clean while a stale last-good .class lingers; a first recompile with no ABI baseline (skipInitial) establishes the full image instead of a half-populated dir; AbiSignature gained type-variable/bounds coverage and DevMojo.lastGeneration became volatile (cross-watcher-thread visibility). The spec’s third diagnostics channel (LSP publish against generated-file URIs, best-effort) did not ship and is filed as R430. Residuals accepted per spec: no live graph-completeness guard (offline oracle only); generated→consumer invalidation is conservative whole-tree pending R333’s method graph, where the sourcing seam re-targets its live exhaustive switches. Full reactor green under -Plocal-db.

  • R426 (3931908 implementation; Backlog → Spec ccbaea5, Spec → Ready b774532, In Progress → In Review 0d01fc1): Project the full parent row for TableRecord-sourced @service children, honoring the already-documented contract of the typed-TableRecord source shape. A @service child whose Sources parameter is a typed TableRecord (Set<FilmRecord>, SourceKey.Wrap.TableRecord) receives keys via env.getSource().into(Tables.X), but the parent $fields SELECT projected only the client’s selection plus R425’s force-included key columns, so a service body reading a non-key column (FilmService.titleTitlecase’s `film.getTitle()) got a silent null whenever the client didn’t happen to select that column; the in-tree execution test passed only because its query selected title alongside, and the federation _entities shape (router selects just the service child) hit the failure in production form. The manual (handle-services.adoc) already promised "fully-populated parent records (every column on the parent table)", so the fix makes the codegen honor the written contract rather than rewriting it. Implementation folds R425’s key-column collection and the new full-row signal into one walk (TypeClassGenerator.collectRequiredProjection) returning a sealed RequiredProjection { FullParentRow | Columns } with an absorbing combine (FullParentRow dominates; "full row subsumes columns" is a type fact, not a dedup accident); build$FieldsMethod switches once, the full-row arm emitting a single Collections.addAll(fields, table.fields()) append that is alias-correct by construction (the caller’s table param carries base column names, the same names into(Tables.X) reads by). Gated on the key wrap (SourceKey.Wrap.TableRecord), not the sealed field variants, so any future BatchKeyField acquiring the wrap gets the right projection for free; sits after R425’s record-parent guard so only table-parent fields reach it. Coverage: pipeline-tier ServiceProjectionPipelineTest R426 group (full-row append for ServiceTableField and ServiceRecordField TableRecord-sourced children via the new TypeSpecAssertions.appendsFullParentRow helper, a Record1-sourced contrast sibling pinning the wrap gating, and a NestingField recursion case); execution-tier GraphQLQueryTest.films_titleTitlecase_withoutSelectingTitle_readsNonKeyColumnOffSourceRecord unmasks the in-tree reproducer ({ films { titleTitlecase } } with no title selected, red pre-fix); federation execution-tier FederationEntitiesDispatchTest.entities_tableRecordServiceChildOnly_nonKeyColumnReadResolvesNonNull (representations-driven fetch, non-key read). Docs: handle-services.adoc contract prose stated plainly, projection-cost note added, and the caveat paragraph’s false table-parent half reconciled. Known residual imprecision noted at review: the full-row append is unconditional in $fields (fires on every fetch of a parent type carrying such a child, selected or not), so the docs' "whenever the field is selected" cost framing understates the trigger; this matches R425’s unconditional force-include shape, and selection-gating the required projection is a possible future refinement. Builds on R425. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R424 (7400c67 core, 1a855a4 rework; Backlog → Spec 889f6cb, Spec → Ready c205824, In Review → Ready rework 814e255, In Progress → In Review a5c485f): Route inline (non-@splitQuery) @reference field argument reads through the field’s own SelectedField instead of the ancestor fetcher’s env. Inside the generated <Type>.$fields(sel, table, env) method, env belongs to the top-level operation fetcher, so env.getArgument("filter") returned null, the filter condition collapsed to noCondition(), and the field silently returned unfiltered rows (data-correctness bug, discovered via an opptak-subgraph reproducer where a @nodeId filter on Studiekurv.kladder was ignored; the @splitQuery sibling behaved because its env genuinely is the field’s own). Fix threads a sealed ArgumentValueSource (Env | FromSelectedField(sfLocal)) through FkTargetConditionEmitter.emitTerm → ArgCallEmitter.buildCallArgs → buildArgExtraction; root/split sites pass Env (byte-identical output), the two inline emitters pass FromSelectedField(sfName) so runtime reads resolve sf.getArguments().get(name). Covers the filter-condition path, the inline first pagination limit, and the JooqConvertlist pre-lift (`+emitJooqConvertKeyLifts, added to both inline emitters by parity ; pre-R424 that inline shape emitted a reference to an undeclared `<name>Keys local; the helper takes FromSelectedField directly since it has no Env caller). ContextArg stays env-based (request-scoped context is legitimately the ancestor env); never-inline arms (InputBean/JooqRecord) guard with IllegalStateException. The $fields host stamps @SuppressWarnings("unchecked") via the source-aware CallParam.emitsUncheckedCastFromSelectedField, keeping the Env hosts' warning-free output unchanged. Coverage: pipeline-tier InlineFilterArgumentSourcePipelineTest (source-aware suppression stamp incl. the top-level JooqConvertlist pin over the non-`@node+` store → customer FK, scalar negative case, end-to-end generation; annotation/model assertions only), execution-tier GraphQLQueryTest against real PostgreSQL (Store.customersByFirstName{,Split} narrowing + inline/split parity, customersFirstN limit, and the decode-consuming CustomerByNodeIdFilter fixture where a foreign store’s node id narrows the inline child list to empty with a @splitQuery parity mirror); compilation tier via the sakila-example -Werror build. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R425 (9c2c9ab implementation; Backlog → Spec 119bfb2, Spec → Ready 2969866, In Progress → In Review 70484cd): Force-include a @service/@splitQuery child’s SourceKey columns in the parent $fields projection so its DataLoader key is never silently null. A @splitQuery child builds its DataLoader key off the parent source record (((Record) env.getSource()).into(<ParentTable>)), but the parent SELECT is driven purely by the client’s GraphQL selection set, so when the client selected the child without selecting a field mapping to the key column, the column was absent from the parent row, key extraction read null, and the child resolved to null with no error, biting hardest under federation where an Apollo Router _entities fetch supplies keys via representations without re-selecting them. Root cause was a pattern-match omission: TypeClassGenerator.collectRequiredProjectionColumns matched the two Split* arms (so split-@reference children were already covered) but let ServiceTableField/ServiceRecordField fall through to Stream.empty(). Fixed by collapsing the two Split* arms into a single BatchKeyField capability arm returning sourceKey().columns() (the capability-interface case per the capability-vs-sealed-switch principle; the enumeration already had a blind spot, its javadoc listed six implementers where seven exist). The three record-parent implementers (RecordTableField, RecordLookupTableField, RecordTableMethodField) are guarded with a loud IllegalStateException rather than routed: they key off a Java accessor via buildRecordParentKeyExtraction and can carry target-aligned columns, so a leak into the table-parent walk would silently project wrong columns, the same silent-null family this item fixes; the guard fails at generation time instead. One refinement beyond the literal spec: a null SourceKey (a @service method taking no Sources param, a plain per-parent delegation with no key read) contributes no columns rather than NPEing. Fold-ins: BatchKeyField javadoc gains the missing RecordTableMethodField; the two force-include taxonomy comments name the capability instead of enumerating Split*. Coverage: pipeline-tier ServiceProjectionPipelineTest (table-bound return ServiceTableField, scalar return ServiceRecordField, and a service child nested under a NestingField, each on a parent with no other force-projecting child, asserting TypeSpecAssertions.appendsRequiredColumn on the parent PK); execution-tier GraphQLQueryTest on a new unmasked City fixture (no @splitQuery/@tableMethod sibling, so the @service children are the only reason CITY_ID is projected) covering cityUppercase (Wrap.TableRecord, the silent-null shape) and cityLowercase (Wrap.Row, the loud-throw shape) queried without any key-mapped field, backed by the new CityService; federation execution-tier FederationEntitiesDispatchTest with a representations-driven _entities fetch selecting only the service child (the opptak reproducer shape, City promoted to a @key entity, FederationBuildSmokeTest’s `_Entity union pin updated); plus a note on the existing Film service-child tests that cast/castByKey mask this behaviour for Film. No code-string assertions on generated method bodies (pipeline tier uses the spec-named appendsRequiredColumn shape helper). Developer-side Wrap.TableRecord contract hazard filed separately as R426; distinct from R424 (child arguments read from the wrong env). Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db.

  • R421 (fb4ca33 guard + tests + pom + seam; 8337c14 test references the seam’s FAULT_HEADER constant; 44fc1b0 move fault seam to a test @Alternative + pin the true single contract; Spec be489aa/514e2f0, Spec → Ready 9927eb8, In Progress → In Review 83d792a, spec wording correction 6ac9f29): Stop GraphqlResource.execute() in graphitron-jakarta-rest from leaking internals when the server-side execution path throws. The resource shaped every request error (400/405/422) into a spec-compliant application/graphql-response+json body but ran the consumer-implemented, auth-seeded application.newExecutionInput() SPI seam and engine.execute() unguarded; a fault there (observed: the seam forcing a JDBC connection with the DB down, a CreationException wrapping PSQLException) escaped past every spec-shaping branch into the container’s generic error handler, dumping the exception chain, stack, DB host/port, and internal package names as a non-spec response. Fixed with an ordered two-arm catch around both calls: catch (WebApplicationException) re-throws unredacted so a consumer signalling a client-facing 4xx from its adapter (e.g. ForbiddenException → 403) has JAX-RS map the intended status rather than collapsing it to a redacted 500 (arm order is load-bearing: WebApplicationException is a RuntimeException, so the broad arm would otherwise swallow it); catch (Exception) mints a UUID correlation id, logs the real cause server-side via SLF4J, and returns HTTP 500 (modern) / 200 (legacy) carrying the reference-only wire shape {errors:[{message:"An error occurred. Reference: <uuid>."}]} with no extensions, byte-identical to the message the generated ErrorRouter.redact emits (ErrorRouterClassGenerator line 481). This resource-level guard is the structural complement to ErrorRouter’s per-fetcher redaction: `newExecutionInput() runs before graphql-java execution begins, the one region neither ErrorRouter nor graphql-java’s own handling can see. The single-contract claim is the reference message, not byte-identity of the whole error object: the fetcher path builds through graphql-java’s GraphqlErrorBuilder which serialises a default extensions.classification, while the resource emits a plain {message} with no extensions, consistent with its own 400/422 errors; matching graphql-java’s classification from the resource was rejected (would contradict the no-extensions requirement and misclassify a pre-execution input-building fault as a data-fetching one). Vendor-neutral: the only dependency added to the module pom is org.slf4j:slf4j-api at provided scope (already version-pinned in the parent dependencyManagement); jakarta.ws.rs.WebApplicationException rides the existing provided jakarta.ws.rs-api, and no RESTEasy/Quarkus type is named. Testing lives in graphitron-sakila-example’s `GraphQLOverHttpConformanceTest (R399: the graphitron-jakarta-rest module carries no @Test classes of its own): four R421 cases (redacted 500 modern with no leaked internals, redacted 200 legacy, WebApplicationException → 403 passthrough, and redactionShapeMatchesFetcherPath pinning message-identity on both legs / no-extensions on the resource leg / classification-present-but-clean on the fetcher leg via the existing Film.durabilityError @service leaf). Fault injection is a test-scoped @Alternative @Priority bean, FaultInjectingGraphitronApplication, which subclasses the real adapter and throws on a sentinel X-Graphitron-Fault header while delegating every other request to super.newExecutionInput(), so execute() still drives the real seam wiring end-to-end and the shipped reference adapter (SakilaGraphitronApplication, a subgraph-author template) stays pristine. No code-string assertions on generated bodies. The trace-correlation follow-up (redaction reference id derived from an OTel trace_id via MDC, rather than a fresh UUID) was deliberately deferred to Backlog R423. Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db (GraphQLOverHttpConformanceTest 15/15, 0 failures across the reactor).

  • R408 (352b05bc implementation, option A; In Progress → In Review 599cbc28, Spec → Ready a3564f2a): Give consumers a build-side lint-finding suppression mechanism, the suppression half of the configurability follow-on R398 deferred. A <lint> block on the Maven plugin config carries two axes with deliberately different scope: <disabledRules> names rule ids to silence everywhere, and <excludedTypes> names type-name globs (* any run, ? one char) to skip in the SDL lint engine’s AST walk. Option A (project config, whole-rule + type-name-pattern disable) was chosen at Ready sign-off over the node-local @lintDisable directive (option B, deferred until a consumer hits granularity a name pattern cannot express) and inline SDL comments (option C, rejected for fragile graphql-java comment-to-node anchoring). LintBinding collapses the POM block into a LintConfig(Set<String> disabledRuleIds, List<String> excludedTypePatterns) record on RewriteContext; LintConfig.validated types each disabled id against LintRule.ids() and fails the build (via MojoExecutionException) naming the offending id(s) and listing the valid namespace, so a typo is a build error not a silently-ignored line. Suppression is applied at the one build evaluator (GraphQLRewriteGenerator.withLintFindings), not in a Maven-log-only filter: the disabled-rule filter runs over the combined BuildWarning list after classifier advisories (schema.warnings()) and engine findings are concatenated, so it keys on the typed rule id and covers both channels (a Source.CLASSIFIER advisory like splitquery-redundant-on-record-parent is suppressible by id like any engine rule); the type-name-glob filter runs inside LintEngine.run, widening the same per-type skip boundary the bundled-type and R407 federation-injected exclusions use, and stays scoped to the engine’s AST walk. That asymmetry is deliberate and pinned by test: the classifier advisories arrive pre-formed with no structured owning-type handle to glob against, so a classifier advisory on an excludedTypes-matched type still fires (reverse-mapping a location or scraping the type name out of message text is the fragile-anchor trap option C rejects). Because the LSP replays the ValidationReport and the MCP diagnostics tool projects it, and DevMojo builds its context through the same buildLintConfig seam, a suppressed finding never surfaces in CI, the editor squiggle, or the MCP tool, from one definition with no second filter. Non-goals held: no severity overrides / error-capable lint (everything stays a warning), no @lintDisable directive, no plugin SPI for new rules. Coverage: pipeline-tier LintSuppressionPipelineTest (5: disabled-rule drops that rule while others fire, excludedTypes skips the matching type but not siblings, glob match, classifier-advisory-by-id suppression, and the engine-scoped excludedTypes asymmetry), unit-tier LintConfigTest (unknown-id validation failure listing the valid namespace, both-axes build, empty config), and single-evaluator parity tests at the LSP tier (LintSuppressionDiagnosticsParityTest: a build-suppressed finding does not replay as a squiggle while a co-present rule still does) and MCP tier (same, through the live diagnostics tool); assertions key on the typed LintRule / lintRule wire field, no code-string assertions on generated bodies. User docs shipped to their real home (docs/manual/reference/mojo-configuration.adoc, the <lint> parameter row plus a "Silencing lint warnings" section). Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db. Builds on R398 (build-is-single-evaluator spine, typed LintRule id); the "author chooses not to fix" sibling of R407’s "cannot fix" exclusion, landing at the same LintEngine.run boundary.

  • R396 (2509653 implementation; In Progress → In Review e79a746, site-6 Spec review 0aeb581, Spec revise 7082fa5, Backlog → Spec 5ff535b): Accept schema-qualified or case-mismatched @table(name:) base names on the @reference FK-connection-and-orientation path. A type declared @table(name: "multischema_a.signal") (or multischema_a.SIGNAL over the real lowercase name) could not attach an @reference(path: [{key: "<fk>"}]) field: the verbatim, case-preserved @table echo was compared by bare equalsIgnoreCase against jOOQ’s always-unqualified FK endpoint names, so the FK read as "does not connect" (Author error: key '<fk>' does not connect to table '<name>'), and where a partial fix let it through, the same bare compare in the orientation predicate silently mis-oriented the join (origin/target swapped, slot pairing inverted). Reported against 10.0.0-RC21 (opptak), the @reference sibling of R395. Fixed by identity comparison, not input sanitization: two new JooqCatalog primitives, foreignKeyTouchesTable (source-side membership, either endpoint) and foreignKeyOnSource (orientation; self-referential FKs fall to the caller’s selfRefHint), resolve the source through the schema-aware findTable and compare FK endpoints by jOOQ table class identity (endpoint.getClass() == resolvedSource.getClass()), falling back to the historical bare compare when the source is Ambiguous/NotInCatalog so the diagnostic surface for genuinely-unknown names is unchanged; class identity also distinguishes same-named tables across schemas, which a normalized bare-name compare cannot. All six spec-enumerated sites routed through the primitives: Phase 1 ({key:} path) parsePathElement connection check → foreignKeyTouchesTable; orientation decided once via foreignKeyOnSource in synthesizeFkJoin and threaded into resolveFkSlots as a precomputed boolean fkOnSource (signature change), so the FK-orientation predicate lives in exactly one place; resolveRecordFkTargetColumns (site 5) uses the primitive for both its implicit-inference directional filter and slot orientation. Phase 2 ({table:} + empty inference) findForeignKeysBetweenTables resolves each argument to class identity; findUniqueFkToTable (site 4) and qualifierForFk (site 6, the synthesis-shim path whose bare re-filter turned a qualified-@table input type into a hard IllegalStateException) re-filter through foreignKeyOnSource. One documented deviation, endorsed on review: site 6 uses foreignKeyOnSource(…, selfRefHint=true) rather than the spec’s suggested foreignKeyTouchesTable, preserving the method’s strictly source-side semantics and the existing qualifierForFk_wrongSourceTable_returnsEmpty test. The verbatim echo stays the source name everywhere, so author-error diagnostics still quote what the user wrote; only the comparison changed. Phase 3 (qualified return-type terminal verdict via TableRef.sameTable) split out to R422 per the spec’s own scope recommendation. Coverage: unit-tier JooqCatalogMultiSchemaTest (both primitives over signal_widget_id_fkey with qualified and upper-case sources, referenced-side/non-endpoint/cross-schema-same-name cases, qualified-both-args findForeignKeysBetweenTables, and a synthesizeFkJoin qualified-source orientation guard pinning origin=signal/target=widget and slot orientation); pipeline-tier QualifiedSourceReferencePipelineTest (all three @reference forms plus the qualified-and-upper-case spelling classify to a correctly-oriented FkJoin, no author error); execution-tier MultiSchemaQueryTest with the R395 fixture tightened from @table(name: "signal") to its originally-specified @table(name: "multischema_a.SIGNAL"), rows still routing to the discriminated types and AlertSignal.widgetName populating through the now-validated cross-table @reference, with R395’s discriminator-qualifier coverage preserved (FROM still renders "multischema_a"."signal"). Known residual, non-blocking: foreignKeysTouchingTable (candidate-hint scoping on the error path) keeps the bare compare, thinning the "did you mean" list for a qualified source without affecting any verdict. Builds on R395 (dependency honored: R395 Done at f6cc9ae before R396 entered In Progress at 3518bb9); spawns R422. Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db (unit 55, pipeline 4, execution 4 in the touched suites; 0 failures across the reactor).

  • R418 (8213374 implementation; In Progress → In Review 2eb9752, Spec → Ready 8d80d72, Backlog → Spec 440b207, Backlog 146d334): Make the web-sandbox SessionStart hook drop-and-recreate rewrite_test on every start, so the fixture DB is a pure function of the checked-out init.sql. Step 2 of .claude/scripts/session-start-web-env.sh previously created + seeded rewrite_test only when the database was absent; a sandbox first seeded from an older init.sql (e.g. one predating R389’s party_* joined-table fixtures + the jti_* composite PKs) kept that stale schema forever, and -Plocal-db jOOQ codegen then built its catalog against the stale DB, cascading into the UnclassifiedType/UnclassifiedField failures (Query.allParties, JoinedTableInheritancePipelineTest, and siblings) that every recent Done review documented as a manual re-seed. The existence guard is replaced by an unconditional DROP DATABASE IF EXISTS rewrite_test WITH (FORCE) (PG13+, cluster is PG16, so a lingering backend from a prior session cannot block the drop) + CREATE DATABASE + reseed, still inside the unchanged pg_ctlcluster/pg_isready gate so local dev (TestContainers, no persistent rewrite_test) stays a no-op; the password-reset line and its pg_was_running guard are untouched, and no checksum/skip optimization is added (sub-second reseed, and a skip-guard would reintroduce the staleness window). .claude/web-environment.md updated: the "Brings up PostgreSQL" step-2 bullet now states the drop/recreate/reseed-every-session behavior, and the Catalog-jar clobber section gains a note that a stale sandbox DB was a second, now-eliminated cause of the same cascade so a future reader does not misfile a DB-staleness failure as a catalog-jar clobber. Web-sandbox tooling only, no reactor/generator/test or init.sql change; the R389 jooq.codegen.schema.version bump is orthogonal and untouched. Verification is manual shell testing (spec § Verification): bash -n parses; the hook run against the live PG16 cluster fully replaces a stale DB (party present, 3 rows; a planted stale marker gone), force-terminates a lingering backend, and two back-to-back runs both succeed. Independent-session In Review → Done review; on the reviewer sandbox the reseeded catalog resolved the party corpus (JoinedTableInheritancePipelineTest 5/5, VariantCoverageTest 3/3 green under -Plocal-db). This item is the standing fix R413/R414/R384/R415/R407/R182 named for the recurring stale-rewrite_test review artifact.

  • R413 (0d4a2d3 implementation; In Progress → In Review 93e57a0, Spec → Ready d4f7401, Backlog → Spec dc18b15): Bind the parent-input VALUES cells of split/reference DataLoader rows methods through the key column’s jOOQ Converter DataType. The rows methods built each parent-key cell straight from the raw key Field (typed by ColumnRef.columnClass(), the converted user type), so a converter-backed or domain-typed key column rendered at the wrong SQL type and the correlation JOIN had no matching operator; against the utdanningsregisteret consumer schema, Campus.ORGANISASJONSKODE (kodeverk.kode_numerisk_domain over BIGINT with a Converter<Long, String>) bound as character varying and every @splitQuery/@reference child nulled out with operator does not exist: kodeverk.kode_numerisk_domain = character varying, invisible on the plain-typed Sakila keys. Fixed at the VALUES-emission seam (the one choke point every key passes through), not at key construction: all four parent-input sites, SplitRowsMethodEmitter.emitParentInputAndFkChain (list/single/connection prelude) + emitFromBridgeAndParentJoin, emitRecordTableMethodBody (@tableMethod variant), buildServiceTableLift (R285 lift-back re-projection), and MultiTablePolymorphicEmitter.buildParentInputValuesEmitter + per-branch ON lookups, now route their cells through ValuesJoinRowBuilder.cellsCode (extended with a constants-class-table-expression variant, making its single-VALUES-cell-authority javadoc claim true), emitting DSL.val(<scalar>, Tables.<OWNER>.<COL>.getDataType()) so jOOQ binds through the registered Converter at the DB type (coerce and cast were verified against jOOQ 3.20.11 and rejected in the Spec). The scalar extraction forks on SourceKey.Wrap, the axis that actually decides value accessors (replacing the coupled reader() instanceof AccessorCall fork): RecordN keys read k.valueN(); RowN keys recover the value from the bind Param via a new per-fetcher-class parentKeyCellValue helper with a loud statement-body throw, the documented contract for @sourceRow lifter keys (unenforceable at validate time, so pinned live at the execution tier). The owner table is a model fact: ParentCorrelation.parentKeyOwnerTable() folds the three-arm fork once (FkJoin origin / LiftedHop target / ConditionJoin parent); the polymorphic arm’s owner (the parent/hub table) rides InterfaceField/UnionField as a non-null parentKeyOwnerTable component threaded from the resolution sites. JOIN-predicate parentInput.field(...) lookups switch to the owner column’s DataType for symmetric type metadata; RowN/RecordN generic type-args stay columnClass()-typed (the converter’s user type IS the Java-side type). Fixtures pin end-to-end: org_code_domain (BIGINT) + converter_org/converter_campus in the fixture DB (schema version 2.3→2.4), OrgCodeStringConverter in graphitron-fixtures-codegen registered via <forcedTypes>, and sakila-example execution tests covering single-cardinality @splitQuery (the reported Campus.organisasjon shape), list-cardinality @splitQuery, and a @sourceRow lifter over the converter-backed key (the live parentKeyCellValue Param-contract pin); existing unit expectations updated in place, no new body-substring assertions. Independent-session In Review → Done review; full reactor green under mvn clean install -Plocal-db (2419 core tests + sakila-example compilation/execution tiers, GraphQLQueryTest 297 tests, 0 failures) after re-seeding a stale local rewrite_test DB (missing the R389 party fixtures and R413’s own converter_* tables), the same Query.allParties / JoinedTableInheritancePipelineTest stale-DB artifact every recent Done review documents (standing fix: R418), on paths R413 does not touch.

  • R414 (f106d52 implementation; In Progress → In Review ae9226b, Spec → Ready c0b4ff7, Backlog → Spec 6ea032b): Serve a real per-parent totalCount on split/DataLoader-backed connections, closing the last classification path that produced null-(table, condition) ConnectionResult carriers on reachable queries. A nested (non-root) @splitQuery connection advertised totalCount: Int in the emitted SDL but always resolved it to null: the scatter path (SplitRowsMethodEmitter.scatterConnectionByIdx) built each per-parent ConnectionResult through the (result, page) convenience constructor, which passed null for the (table, condition) pair the generated ConnectionHelper.totalCount needs to issue its SELECT count(*). The fix mirrors the B4c-2 polymorphic-batched semantics (MultiTablePolymorphicEmitter.buildBatchedConnectionRowsMethod): the rows method hoists its WHERE into a single Condition where local (buildWhereCondition called exactly once, since it declares FK-target alias locals as a side effect) shared by the windowed page query and a new cursor-independent countSource derived table (same join topology via emitFromBridgeAndParentJoin, no orderBy/seek so the count is window-independent); scatterConnectionByIdx gains a Table<?> countSource parameter and binds each per-parent carrier via the 4-arg ConnectionResult constructor with countSource.field("__idx__", Integer.class).eq(DSL.inline(i)), so ConnectionHelper.totalCount runs SELECT count(*) FROM countSource WHERE __idx__ = i lazily on selection (zero count SQL when unselected, N counts for a batch of N parents when selected). The now-dead two-arg (result, page) ConnectionResult constructor is removed (scatterConnectionByIdx was its only caller; this is generated-into-consumer source, not published API); the nullable (table, condition) field shape and the if (cr.table() == null || cr.condition() == null) return null guard stay for the one validator-unreachable producer that remains, MultiTablePolymorphicEmitter.buildRootConnectionFetcher’s defensive empty-participants `new ConnectionResult(List.of(), page, null, null), and the three "Split-Connection scatter passes null" comments are re-pointed to name that remaining producer rather than narrowed to non-null. Sakila’s ActorsConnection (shared by Film.actorsConnection + Film.actorsOrderedConnection, both @splitQuery) gains totalCount: Int. True B4c-2 structural unification (a shared materialised pre-window derived table feeding both the ranked window and the count) is a deliberate non-goal: the split page query’s .orderBy(page.effectiveOrderBy()).seek() reference live terminal-alias columns, and re-pointing them at derived-table fields would rework well-tested pagination for no user-visible gain; the dual topology emission shares one emitFromBridgeAndParentJoin helper and the single hoisted where, so drift risk is low (possible follow-up if it ever drifts). Coverage: execution-tier GraphQLQueryTest (splitQueryConnection_totalCount_isParentScoped counts 2/2/1/1/1 for films 1-5 against the seeded film_actor rows with first: 1 to distinguish the count from the page size; _isCursorIndependent still reports 2 paging past an after cursor; _isLazyOnSelection asserts no select count SQL when unselected and exactly N per-parent count statements when selected) and pipeline-tier SplitTableFieldPipelineTest (structural: scatterConnectionByIdx carries the Table<?> countSource parameter), no code-string assertions on generated method bodies. Mirrors B4c-2’s count semantics; per-parent totalCount for hypothetical future split shapes (SplitLookupTableField has no connection arm today) stays out of scope. Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db (2418 core tests + the sakila-example compilation/execution tiers, 0 failures). The graphitron-core red first seen in the review sandbox (Query.allParties → UnclassifiedField; JoinedTableInheritancePipelineTest ClassCast on the R389 party/jti_* fixtures) was the known stale-rewrite_test-DB artifact every recent Done review documents, the local DB predating the R389 fixtures and jOOQ’s schemaVersionProvider suppressing catalog regeneration; re-seeding from init.sql + a clean catalog regen produced a fully green reactor including R414’s execution tier, on paths R414 does not touch (the R418 always-reseed hook is the standing fix).

  • R384 (60b58ae phase 0 plumbing, 6021bda phase a JooqConvert, fb1b2e4 phase b NodeIdDecodeKeys, feadbcb phase c developer @condition, 24b2527 In Progress → In Review; Spec → Ready re-review bd2e761): Lift the remaining filter-argument kinds onto multitable interface/union root query fields, completing the arc R363 (branch-safe Direct/EnumValueOf/ContextArg) and R383 (nested-input @field) opened. Where R383 needed zero new plumbing, these three kinds share it, so the work is one multi-phase item over a single seam: phase 0 widens MultiTablePolymorphicEmitter’s root entry points to carry the enclosing `<Type>Fetchers class’s CompositeDecodeHelperRegistry and pre-declares (as statements ahead of the inline stage-1 union) the locals branch filter terms cannot introduce themselves, FK-target join-hop aliases (FkTargetConditionEmitter.declareAliases per participant, namespaced by the stage1_<Type> base), deduped <name>Keys locals for JooqConvert-list args, and shared lifted-outer Map locals, replacing branchFilterWhere’s former `emitTerm(..., null, null, Map.of()) with the threaded values (behaviorally inert, proving the seam before any arm flips). Phase a flips the JooqConvert arm branch-safe and, per R267 (fix a deprecation-for-removal at the source, never suppress), replaces the deprecated-for-removal DataType.convert(Object) in ArgCallEmitter’s shared arm with the non-deprecated `DSL.val(raw, col.getDataType()).getValue() coercion (the design-principles §"Column value binding" idiom, .equals-identical to the deprecated form on a converted domain type, verified against jOOQ 3.20.11), correcting the single-table path in the same change; it also aligns the nested @field leaf with top-level conversion semantics (an ID-typed nested @field over a plain column now carries a JooqConvert leaf instead of the hardcoded Direct) and carves JooqConvert leaves out of CallParam.emitsUncheckedCast (the instanceof List<?> guard casts nothing). Phase b flips NodeIdDecodeKeys branch-safe, homing the drained decode helpers on the <Type>Fetchers class hosting the branch call site (a documented revision of the Ready text’s per-participant-composer wording, which put wire-decode machinery on the env-free pure-function composer and was not implementable; the per-class collectInto bracket is the true single-table precedent). Phase c removes the R363 field/arg-level hasCondition pre-guard and relaxes firstUnsupportedFilterArg’s first guard so developer `@condition filters (ConditionFilter/FkTargetConditionFilter) gate uniformly on their per-param extractions; the developer method reflects once per participant and runs against each branch’s stage-1 alias, a Table<?> first parameter serving every branch while a concrete-table parameter surfaces a mismatch at consumer javac (R379 semantics). The exhaustive nine-permit isBranchSafeExtraction switch stays the forcing function: NodeIdDecodeRecord/InputBean/JooqRecord remain explicit false arms (mutation-input/record-decode shapes that do not occur as a multitable root-query filter arg). Coverage: pipeline-tier MultiTableFilterLoweringTest (each kind’s rejection test flips to a lowered per-participant assertion on the model, JooqConvert/NodeIdDecodeKeys extraction, nested-leaf alignment, three @condition cases including nested-input) and execution-tier MultiTableFilterExecutionTest over the AddressOccupant = Customer | Staff union (occupantsByStoreId per-branch coercion, occupantsByAddress decode-and-filter plus a wrong-type-id client-error, occupantsStartingWithM/occupantsByNamePrefix/OccupantFilter.namePrefix via the new Table<?>-generic MultiTableConditionFixtures), with graphitron-sakila-example’s `-Xlint:all -Werror compile the pin that phase a stays off the deprecated form; no code-string assertions on generated bodies. Independent-session In Review → Done review; full reactor green under mvn clean install -Plocal-db (0 failures) after re-seeding a stale local rewrite_test DB (missing the R389 party fixture) and regenerating the jOOQ catalog, the same Query.allParties / JoinedTableInheritancePipelineTest stale-DB artifact earlier Done reviews noted, on paths R384 does not touch. Builds on R363 / R383; pins jOOQ coercion with R267 / R379.

  • R415 (4be03d6; Spec → Ready 197307d, Backlog → Spec 51503c5): Clamp connection first/last at the single runtime choke point and unify the no-channel error disposition. A negative first/last on a connection field flowed unvalidated into the SQL LIMIT, so PostgreSQL threw LIMIT must not be negative, which the framework redacted into an opaque correlation-id 500 instead of a client-facing validation error; first: 2147483647 produced the identical redacted 500 via limit = pageSize + 1 wrapping to Integer.MIN_VALUE. Fixed in the emitted ConnectionHelper.pageRequest (the one choke point every connection flavour funnels through): three guards next to the existing mutual-exclusion check, all throwing the R378 client-error marker GraphitronClientException so the real message reaches the client, negative first, negative last, and the derived-limit overflow (pageSize == Integer.MAX_VALUE, guarding the value PostgreSQL actually enforces rather than each input, which also covers a pathological defaultPageSize); the pre-existing mutual-exclusion IllegalArgumentException("first and last must not both be specified") migrated onto the same marker (same client-mistake family, same redaction defect). first: 0 stays valid. The purity note reworded to the load-bearing property (pageRequest takes no DataFetchingEnvironment; the marker subclasses GraphqlErrorException, so "no graphql-java dependency" was never the true invariant) at both ConnectionHelperClassGenerator and TypeFetcherGenerator:4792. Second defect, drift R378 introduced: the no-channel disposition is one decision at four emit sites across two emitters, and R378 flipped only the two sync catch arms to surfaceClientErrorOrRedact while leaving the two async .exceptionally arms on plain redact, so a client error on a nested (DataLoader-based) @splitQuery connection would still redact. Lifted the router call into one shared definition ErrorRouterClassGenerator.noChannelRouterCall(outputPackage, throwableVar) consulted by all no-channel sites, both noChannelCatchArm`s, both `asyncWrapTail no-channel branches, and ChannelCatchArmEmitter (a fifth site already on the right disposition); the two async arms flip to surfaceClientErrorOrRedact, whose cause-chain walk unwraps the CompletionException DataLoader wraps around a batch-function throw, so the marker surfaces while everything else keeps redacting (blast radius bounded to the marker type). The next disposition change is now one edit, not a four-site hand-coordination. Prose that named the old behaviour fixed in the same pass (TypeFetcherGenerator:6303/:6498, MultiTablePolymorphicEmitter:2006). Coverage: execution-tier GraphQLQueryTest (filmsConnection negative first/last surface the argument-naming message and never contain An error occurred. Reference:, first: 2147483647 surfaces the overflow message, first: 0 returns an empty page with a computable hasNextPage, the migrated collision message, and the load-bearing filmById(...) { actorsConnection(first: -1) } proving the async-arm flip end-to-end through the DataLoader CompletionException unwrap, which a root-connection test alone would pass without the flip), unit-tier ErrorRouterClassGeneratorTest.noChannelRouterCall_emitsSurfaceClientErrorOrRedact, and updated TypeFetcherGeneratorTest arm pins. Out of scope, filed nowhere yet: malformed after/before cursor redaction (same family, different surface) and a configurable maximum page size (a DoS policy cap, distinct from this correctness-only overflow guard). Builds on R378. Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db (GraphQLQueryTest 294 tests, 0 failures) after re-seeding a stale local rewrite_test DB (missing the R389 party + jti_* joined-table fixtures) and a clean jOOQ catalog regen, the same Query.allParties / JoinedTableInheritancePipelineTest stale-DB artifact the R416/R407/R182 Done reviews documented, on paths R415 does not touch.

  • R416 (71f5429 In Progress → In Review implementation; d58ae37 Dependabot #504/#506 closure record; d893673 self-review charset-tolerant asset content-type assertions; Spec → Ready 943884f, Backlog → Spec 9ad3ff1, Backlog c3623f2): Self-host the GraphiQL playground assets in graphitron-jakarta-rest, retiring the runtime unpkg CDN. The playground page (GET /graphql, Accept: text/html) previously loaded React + GraphiQL from unpkg at latest (unpinned, so it silently tracked upstream) and was dead behind a strict CSP or on an air-gapped network, unacceptable for "the first hand-written runtime artifact consumers depend on" serving Sikt’s gov/edu consumers. Now a version-pinned GraphiQL 5 + React bundle (graphiql@5.2.2, react@18.3.1, graphql@16.13.2, @graphiql/toolkit@0.11.3, built with vite@6.4.2 / @vitejs/plugin-react@4.7.0) ships as plain classpath resources under no/sikt/graphitron/jakarta/rest/graphiql/ and is streamed by a new path-traversal-safe @GET assets/{name} method on GraphqlResource (a [A-Za-z0-9._-]+ allowlist + explicit .. reject + extension→MIME gate over js/css/map/ttf/woff/woff2/svg, all behind the existing graphiqlEnabled() seam). Vendor-neutral by design: getResourceAsStream, not META-INF/resources/ (which only serves on Quarkus, contradicting the module’s Jakarta-EE-neutral ethos). graphiql() gained @Context UriInfo and rewrites a {{ASSET_BASE}} placeholder to the absolute per-request .../graphql/assets/ prefix, so the entry files resolve at any mount point (/graphql, /api/graphql, …​) while every code-split chunk/worker/codicon-font resolves relative to them via the bundle’s base: './'. The Vite recipe was git mv’d from the (now GraphiQL-free) `graphitron-sakila-example into graphitron-jakarta-rest/tools/graphiql-build/ as a one-shot commit-the-output recipe: no <build> binding, so the reactor’s "CI never touches node" property holds; the committed bundle is the artifact, the recipe the reproducibility receipt. Deviations from the plan, all documented and justified: a JS-entry Vite build (GraphiQL 5 is a monaco bundler SPA, not the old UMD global-script shape) rather than the literal "swap four URLs"; staying on vite 6 / plugin-react 4 rather than #506’s vite 8 / plugin-react 6 majors (plugin-react 6 drops Babel for Oxc, a larger change with no benefit here); the MIME map extended past the plan’s js/css/map to cover monaco’s codicon .ttf and fonts; the broader R399 app-section README drift (dead GraphqlEngine/GraphqlResource/AppContext links) split out as follow-up R417 rather than expanded into scope. The companion/opt-in -graphiql artifact (the architect’s leanness-preserving "option C") was consciously deferred: the consumer set is small and known, so the bundle weight is accepted in the core jar now, with option C the escape hatch if a real consumer is pinched. Docs reconciled across every GraphiQL surface (the graphiql.html rationale comment, the relocated recipe README, modules.adoc, both tutorial pages, the sakila-example README’s GraphiQL entries); Dependabot #504 (linkify-it) and #506 (vite/plugin-react) closed as superseded, each pointing at R416. Coverage per R399 (jakarta-rest carries no @Test classes): graphitron-sakila-example’s `GraphqlResourceSmokeTest gained six page + asset conformance checks (self-hosted page has the mount div, no unpkg.com, and a resolved .../graphql/assets/ base; the entry graphiql.js/graphiql.css stream with the right content-type; missing and unknown-extension names 404). Independent-session In Review → Done review; full reactor green under mvn clean install -Plocal-db (2416 core tests + the sakila-example compilation/execution tiers, GraphqlResourceSmokeTest 6/6, 0 failures). The graphitron-core red the implementer reported as "pre-existing trunk breakage" (Query.allParties → UnclassifiedField, JoinedTableInheritancePipelineTest, and the Person/AppAccount joined-table schema-validation failures in sakila-example generation) was in fact the known stale-rewrite_test-DB artifact the R407 and R182 Done reviews already documented, the local DB predating the R389 party + jti_* fixtures so its jOOQ catalog lacked the detail tables' primary keys; re-seeding from init.sql + a clean catalog regen produced a fully green reactor and let the conformance test run in-pipeline, on paths R416 does not touch.

  • R407 (406f80d implementation; In Progress → In Review 2f5f741, Ready → In Progress d910a1f, Spec → Ready 825b524, build-command fixup b483e1e): Exclude generator-injected federation/@link definitions from the R398 SDL lint engine. A consumer schema carrying a federation @link saw type-names-pascal-case and types-and-fields-have-descriptions warnings on federation__FieldSet, link__Import, and siblings the author never wrote and cannot rename (names dictated by the federation spec) or document (descriptions owned by federation-graphql-java-support); these definitions carry a null source, the tell that they came from no consumer .graphqls. Fixed by provenance, not a name.contains("__") heuristic and not by borrowing ScalarTypeResolver.FEDERATION_NAMESPACE_SCALARS (a hand-maintained expectation free to drift on a spec bump): FederationLinkApplier.apply now returns the Set<String> of names it injected (collected in its existing defs.forEach loop, the sole contributor) instead of a bare boolean. AttributedRegistry carries that set as injectedNames() and derives federationLink() from it ("injected anything"), collapsing the two facts into one component rather than a parallel carrier; AttributedRegistry.from(...) derives the set the same way for ad-hoc test registries. LintEngine.run gains an overload taking the set and unions it with the existing BUNDLED_TYPE_NAMES exclusion at the two skip points it already has, widening the name-set skip to a second generator-owned contributor with no new skip mechanism, no new LintRule, no new LintNodeKind. KeyNodeSynthesiser is untouched: it decorates author @node types in place with @key and injects no new definitions, so folding its names in would wrongly silence real author violations; it keeps its void signature. Coverage: pipeline-tier LintInjectedFederationDefinitionsTest carries a federation @link plus an author type lowercase @node (the exact type synthesis decorates), exercising the real injection path and pinning both halves in one fixture, injected names stay silent while the author type’s pascal-case violation still fires; findings asserted on the typed LintRule and the minimum node-identity check, no rendered-message or generated-body assertions; FederationLinkApplierTest updated to assert on the returned name set. No per-name unit list. Reuses R398’s BUNDLED_TYPE_NAMES mechanism; sibling to R408 (author-driven suppression, the "chooses not to fix" half). Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db (2407 core tests + sakila-example compilation/execution tiers, 0 failures) after re-seeding a stale local rewrite_test DB (missing the R389 party + jti_* joined-table fixtures) and regenerating the jOOQ catalog, the same Query.allParties / JoinedTableInheritancePipelineTest stale-DB artifact earlier Done reviews noted, on paths R407 does not touch.

  • R182 (In Review → Done; landing sequence 657dd71 delete legacy reactor, a505cab unwrap rewrite to repo root, aad2f61 restructure architecture docs into Diataxis docs/architecture/, cbaeb64 CI/CD workflows, 31750bf docs+tooling paths; In Review transition b0cb778; In Progress e90e321; R19 discard adb071a): Retired the legacy graphitron-parent reactor and unnested graphitron-rewrite/ to the repo root. The six legacy modules (graphitron-codegen-parent, graphitron-common, graphitron-example, graphitron-maven-plugin, graphitron-servlet-parent, graphitron-schema-transform) and the legacy root pom.xml + maven-build.yml are gone; the rewrite aggregator (graphitron-rewrite-parent) is now the root POM with its eleven modules + docs at the top level, and the duplicate graphitron-javapoet collapses to one copy. This closes the release-event publish hazard: maven-publish.yml on main drops the -f graphitron-rewrite/pom.xml flag and carries the RC-aware tag regex, so a release tag can no longer republish deleted legacy artifacts at 10.0.0. Docs restructured: the flat graphitron-rewrite/docs/ tree became a Diataxis-shaped docs/architecture/{explanation,reference,how-to}/ folded into the site module (README.adoc split into index.adoc + reference/modules.adoc + explanation/pipeline-overview.adoc; getting-started.adoc dissolved into the manual how-to pages plus a new how-to/dev-loop-internals.adoc carrying the #dev-loop-detail / #native-runtime-dependency anchors), and roadmap-internal workflow.adoc moved out of the site to roadmap/workflow.adoc. roadmap-tool’s `Main.java gained a quadrant-aware mapAdocTarget (driven by an ARCH_QUADRANT slug table) plus repointed README/status-board headers; the ~11 inbound docs/** architecture xrefs were repointed; CI path prefixes and the tree-sitter-natives-release.yml env vars were de-prefixed. verify-standalone-build.sh retired, the CLAUDE.md legacy-scope rule removed, and rewrite-design-principles.adoc’s standalone-vs-legacy invariant reworded now that no legacy tree exists. Closes R26’s last open sub-item and supersedes R19 (discarded, not squashed). Step 8 (cut a release tag to exercise the consolidated publish workflow end-to-end) is inherently post-merge: the hazard only closes once this lands on `main. Independent-session In Review → Done review; full reactor green under mvn install -Plocal-db (2406 tests in graphitron, docs site renders the Diataxis /architecture/ tree with workflow.adoc correctly absent). The graphitron-core red first seen in the review sandbox (Query.allParties → UnclassifiedField) was the known stale-rewrite_test-DB artifact (the pre-existing DB predated the R389 party fixture and jOOQ skipped codegen against it); re-seeding from init.sql + a clean catalog regen produced a fully green reactor, orthogonal to the R182 diff.

  • R19 (discarded, superseded by R182): "Rebase and squash rewrite branch onto main" is abandoned. R182 retires the legacy reactor and unnests the rewrite by moving the tree up one level in three ordinary commits (delete, unwrap, docs restructure), not by rewriting history; R19’s squash approach is not the accepted path. R19’s own numbers were stale (April-2026 commit counts against a since-moved merge base). File deleted per the 2026-07-01 staleness audit.

  • R406 (landing e72eb02; rework onto R405 trunk from first pass 1b6971d, In Review → rework 1f4412f, Spec → Ready 6296d0b, spec 5f5ebc2, filed 1633934): Support a single-table discriminated interface (@table @discriminate, implementers pinned by @discriminator(value:), all sharing one jOOQ table, e.g. Content over content) as a DML @mutation(typeName: INSERT|UPDATE) return type. Before this, such a return was not rejected but silently mis-accepted (a TableInterfaceType is a TableBackedType, so it classified through the TableBoundReturnType arm to ProjectedSingle/ProjectedList) and emitted a <Type>.$fields(...) re-projection that never generates for an interface, so the sources failed to compile. The write half is a plain single-@table write (the discriminator is an ordinary @field(name: "CONTENT_TYPE") input column the client sets; resolveInput unchanged); the entire fix is on the return half. Model: added the sibling DmlReturnExpression.DiscriminatedSingle/DiscriminatedList arms carrying the read-side discrimination data (interfaceName, discriminatorColumn, knownDiscriminatorValues, TableBound participants) sourced verbatim from the TableInterfaceType verdict, the DML sibling of R405’s *ServiceTableInterfaceField; both map to the same Record/Table domain-return + target shape as Projected*. A new return-shape arm (not a per-verb MutationField leaf) keeps the fork off the write-verb axis, per "lift the fork into the model": the write half is uniform across INSERT/UPDATE and the model already carries the return-shape seam. Classify: the single DML chokepoint buildDmlField resolves the return’s look-ahead verdict once and threads it into the still-static buildDmlReturnExpression, which builds the Discriminated* arm when the verdict is a TableInterfaceType. Validate: dispatchPerformsReFetch recognises the Discriminated* arms as re-fetching, keeping the emitter and OutputField.requiresReFetch() in lockstep under the build-time drift guard; the DELETE and @asConnection floors already fire for the interface case through the shared TableBoundReturnType arm. Emit: consumes R405’s shared read-side re-projection TypeFetcherGenerator.buildTableInterfaceReprojection (passing List.of() for alwaysProject, since the DML path keys the follow-up SELECT by a PK-IN Condition off the RETURNING keys rather than re-mapping by PK like the service path); the duplicate buildDiscriminatedReprojection from the first pass is deleted. Step 1 (PK-only RETURNING in dsl.transactionResult) and the composite-safe PK-IN builder were extracted into emitKeysTransaction + buildPkKeysCondition, R406-owned and shared only between emitProjected and emitDiscriminated (the DML write half has no R405 equivalent; they key off the RETURNING keys local, distinct from R405’s records-keyed MultiTablePolymorphicEmitter.buildPkInCondition). The generated row carries __discriminator__; the interface’s existing TypeResolver sets __typename per row, so no new resolver and no per-typename UNION. Drop/write-read asymmetry aligned with R405: an INSERT of an unknown discriminator commits its row (the transaction closed before the follow-up SELECT) yet returns null, since the discriminator filter cannot name a subtype outside the known set; enforcing the discriminator domain is the database’s job (a CHECK constraint), not a graphitron pre-screen. Scope: INSERT/UPDATE only; DELETE (encoded-ID, own floor), UPSERT (blocked on R144/R145), unions (permanently), and Connection stay out. Coverage: unit-tier GraphitronSchemaBuilderTest (DiscriminatedSingle INSERT + UPDATE + DiscriminatedList classification, a regression pin against the pre-R406 ProjectedSingle silent-accept, and a DELETE-floor rejection pin), pipeline-tier FetcherPipelineTest INSERT/UPDATE shape assertions, and execution-tier DmlTableInterfaceReturnExecutionTest (real PostgreSQL over the content fixture + a new ContentInput/createContent/updateContent schema fixture: per-__typename routing off the live discriminator, the cross-table FilmContent.rating join, same-table ShortContent.description isolation, and the unknown-discriminator write/read asymmetry). Independent-session In Review → Done review; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db. The full-reactor graphitron-core red first observed in the review sandbox (Query.allParties → UnclassifiedField; JoinedTableInheritancePipelineTest ClassCast on the R389 jti_* fixtures) was the known stale-rewrite_test-DB artifact (missing jti_app_account/jti_person composite PKs) compounded by jOOQ’s up-to-date codegen skip; re-seeding from init.sql + a clean catalog regen produced a fully green reactor including R406’s execution tier, and the failures are orthogonal to R406. Consumes R405’s buildTableInterfaceReprojection (extracted read-side, landed 7b9d051). Non-blocking carry noted at review: the two FetcherPipelineTest cases use CodeBlock.toString().contains(...) body-string assertions (existing DML-fetcher precedent in the same file; model shape pinned in GraphitronSchemaBuilderTest, behaviour at the execution tier).

  • R370 (core four sites + fixtures + R412 filing a13523e; two remaining in-hand @service-path sites + witnesses 4e22ee4; Spec → Ready 7a853a5, In Review → Ready rework 352cc16): A record-backed parent with a nested backing class emitted the non-compiling $-qualified Outer$Nested because ClassName.bestGuess re-parses a binary class name and never splits on $. Fixed at the six sites that already hold a structurally-correct name in hand, so the swap is a one-for-one ClassName.get(Class) / captured-TypeName substitution at the source boundary rather than a per-consumer patch: the two AccessorRef producers (deriveAccessorRecordParentSource, derivePolymorphicHubSource), the @service return-type validator (checkServiceReturnMatchesPayload, previously spuriously rejecting nested payloads at classify time), and the query + mutation @service fetcher return types (computeServiceRecordReturnType, computeMutationServiceRecordReturnType, both collapsed to ServiceMethodCall.javaReturnType() so the twins no longer drift), plus the @service Outcome payload ctor arm (resolveErrorChannel → ClassName.get(payloadCls)). Restores AccessorRef’s own javadoc contract and corrects the now-true "Identical policy" mirror javadoc; the stale R370 hazard note in `buildScalarPerParentFetcher was removed. Coverage: two compilation-tier fixtures, one per AccessorRef producer (NestedFilmsPayload list-arm via buildAccessorKeyMany, NestedOccupantCarrier single-cardinality polymorphic via buildScalarPerParentFetcher); a mutation + @error-channel compilation fixture (NestedFilmReviewPayload) reaching computeMutationServiceRecordReturnType and resolveErrorChannel; and a classification-tier ErrorChannelClassificationTest.childServiceRecordField_nestedPayloadBacking_payloadClassIsStructurallyResolved pinning the resolved PayloadClass.payloadClass() to the structural Outer.Nested by object-equality on the TypeName (no code-string assertion on any generated body). The review corrected the spec’s second-witness premise: after the R244 Outcome flip, root @service outcome fields classify to ErrorChannel.Mapped (no developer payload class emitted), so resolveErrorChannel’s `PayloadClass arm is reached only by a child @service field, hence the classification-tier witness rather than a sakila compile fixture. The remaining bestGuess-over-fqClassName emit sites that hold no reflected Class/captured TypeName (backingClassOf, recordColumnReadArgs, FetcherEmitter, several ChildField sites) need a model-lift, not a call swap, and were filed as R412. Independent-session In Review → Done review; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db (2406 core tests + the sakila-example compilation/execution tiers, 0 failures) after re-seeding a locally-clobbered rewrite_test catalog (the Query.allParties / JoinedTableInheritancePipelineTest cascade was a stale-DB artifact orthogonal to R370).

  • R405 (landing 7b9d051; Spec → Ready e133e55, Backlog → Spec ca05952, flow/mechanism capture f61566b, filed 1633934): Support a single-table discriminated interface (@table @discriminate, implementers pinned by @discriminator(value:), all sharing one jOOQ table) as a root @service polymorphic return, closing the last deferred shape on the @service polymorphic surface. Route (a) (R365, multitable) dispatches each service-returned record on its runtime Java class, which cannot tell same-table subtypes apart; this path instead reuses the read-side discriminator mechanism. New QueryServiceTableInterfaceField / MutationServiceTableInterfaceField leaves (single-table siblings of *ServicePolymorphicField) carry the shared-@table TableBoundReturnType, discriminator column + known values, and TableBound participants plus the service binding; target shape Interface keeps requiresReFetch() false. Both root classifiers' TableBound arms build the new variant when the verdict is a TableInterfaceType (deferServiceTableInterface deleted). Emit reuses the read-side projection: the shared discriminator-filter + __discriminator__ projection + cross-table LEFT JOIN assembly was extracted from the two read fetchers into a package-private TypeFetcherGenerator.buildTableInterfaceReprojection (read paths pass an empty alwaysProject, output unchanged); the new MultiTablePolymorphicEmitter service fetcher calls the service, normalises the return to List<Record> (extracted shared snippet), collects the shared table’s PKs into a composite-safe DSL.row(pk…).in(rows) condition, runs one by-PK SELECT through the shared helper, and re-maps rows to input positions by PK (drop contract aligned with route (a): an unmatched PK drops from a list, yields null for a single). Validate mirrors the single-table floor (validateCardinality only, not the multi-table participant check). Union returns stay permanently unsupported; child @service polymorphic returns and @asConnection stay out of scope. Coverage: pipeline-tier ServiceTableInterfaceReturnPipelineTest, unit-tier classification flip (serviceReturningTableInterface_classifiesAsServiceTableInterfaceField + mutation twin) and ServiceTableInterfaceFieldValidationTest, the two ClassifiedCorpus examples + GeneratorCoverageTest leaf-partition pin, and execution-tier ServiceTableInterfaceReturnExecutionTest (real PostgreSQL: list routes each row to FilmContent/ShortContent off the live discriminator, populates FilmContent.rating via the cross-table join, honours the drop contract, single + mutation cardinality). Independent-session In Review → Done review. R405’s own tests are green; the full-reactor graphitron-core red observed in the review sandbox (Query.allParties → UnclassifiedField; JoinedTableInheritancePipelineTest ClassCast) was a stale-rewrite_test-DB artifact (missing party_* tables + jti_person PK) compounded by jOOQ schemaVersionProvider regeneration caching, reproduced identically at the pre-R405 parent 8fca064 and orthogonal to R405; re-seeding the local DB from init.sql + a clean rebuild produced a fully green reactor including R405’s execution tier. Non-blocking follow-up noted at review: the pipeline test uses CodeBlock.toString().contains(...) body-string assertions (repo precedent exists; behaviour is independently pinned at the execution tier). R406 reuses this item’s read-side dispatch for its DML return half.

  • R332 (cb99991 implementation; 8fca064 fixture-gate accounting; Spec → Ready 6ad75dd): Deprecation signal for @table on input types, a signal-only precursor ahead of R97’s consumer-derived removal (classification behaviour unchanged). Two tiers per D1. Prose tier (ships unconditionally, carries the carve-out in words): the @table description in directives.graphqls gains the input-type deprecation note + replacement instruction (no SDL @deprecated marker, the spec forbids it on a directive location); a directive-level row in deprecations.adoc with the section heading/intro widened from "whole directives" to "whole directives and directive locations"; a WARNING admonition on the canonical table.adoc page; and the code-generation-triggers.adoc Input type with @table row annotated. Actionable tier (fires per @table-on-input type, respects the carve-out): a post-classification GraphitronSchemaBuilder.emitTableOnInputDeprecationWarnings(ctx) pass placed beside rejectCaseInsensitiveTypeCollisions, walking ctx.schema input types that explicitly declare @table and emitting a non-fatal BuildWarning.NoRule per usage (the plain non-lint arm per D1, not LintFinding; message names no R<n> per D2; unconditional per D4). The encoded-ID / scalar-return INSERT/UPSERT carve-out (D3) is computed by encodedWriteTargetInputTypes(ctx) off the classified model (the MutationInsertTableField/MutationUpsertTableField leaves whose returnExpression() is an Encoded* arm), needing no lookAheadVerdict or reflection, so R332 stays depends-on: []; the type-level conservative rule suppresses an input reused by any encoded INSERT/UPSERT. encodedWriteTargetInputTypes is the named find-usages anchor R97 Phase 2b retires (forward edge R97 → R332). Coverage: pipeline-tier TableOnInputDeprecationWarningTest (projected-return INSERT warns with source location; encoded-ID INSERT carved out; D3 reuse suppression) 3/3 green; DeprecationsDocCoverageTest gains "table" in WHOLE_DIRECTIVE_DEPRECATIONS; FixtureWarningsGateTest segregates the R332 category out of its exactly-one advisory scope (mirroring the ENGINE-lint filter) and adds a count-independent carve-out test (FilmCreateInput/FilmDeleteInput warn, encoded-ID CreateKeyedNodeInput does not); no code-string assertions on generated bodies. Independent-session In Review → Done review; TableOnInputDeprecationWarningTest green and the emitted NoRule message shape matches both gate matchers by inspection. The two graphitron-sakila-example tests could not execute because that module’s generate goal is blocked on trunk by the pre-existing R389 fixture breakage (party / party_individual / jti_person catalog tables absent, Query.allParties unclassified); the 11 graphitron-core failures (JoinedTableInheritancePipelineTest ClassCast + the Query.allParties corpus cascade) were confirmed to reproduce identically with R332 reverted, so they route through R389, not this item. Remaining work: R97 Phase 2b empties the carve-out and lets the warning fire on encoded INSERT/UPSERT inputs, at which point this item folds or retires.

  • R398 (1d520fa engine + nine visitors, 82fd246 sealed BuildWarning + advisory tagging + MCP projection, 148c8ac LSP finding-keyed QuickFix, fd38fb0 @record deprecation-marker alignment, 53d475b report/LSP integration pins, d5b0a57 In Progress → In Review, 3a4a190 explicit-fix decoupling; Spec → Ready 3ee7062): SDL lint engine with ESLint-style built-in visitors. A single shared traversal over the build’s parsed graphql-java AST dispatches each node to the LintVisitor`s subscribed to its `LintNodeKind; adding a rule is registering it in LintRules, not editing a central switch. Rule identity is a type (LintRule enum, stable kebab-case id(), a Source{ENGINE, CLASSIFIER} axis), never a string bag. Findings ride the existing warning channel via a sealed BuildWarning (NoRule arm for the pre-existing untagged advisories; LintFinding arm carrying the typed LintRule + Optional<LintFix>), so a finding’s rule is a type and its fix lives only on the arm where it is meaningful, no nullable field; both arms flow into ValidationReport unchanged, so the LSP replay (Diagnostics.validatorDiagnostics, R139 freshness-silence intact) and the MCP diagnostics tool project findings with no second evaluator, the MCP wire additionally carrying the LintRule id. Nine syntactic engine visitors ship (type-names-pascal-case, field-names-camel-case, input-and-argument-names-camel-case, enum-values-screaming-snake-case, deprecations-have-a-reason, types-and-fields-have-descriptions, input-object-name-suffix, no-deprecated-directive-usage, no-typename-prefix); the three existing classifier advisories (splitquery-redundant-on-record-parent, redundant-record-directive, asconnection-same-table-pk-in) are surfaced and tagged at their FieldBuilder/TypeBuilder emit sites, never re-derived, so the classifier stays their sole emitter and each coordinate is warned exactly once (no-deprecated-directive-usage excludes @record, owned by the redundant-record advisory). Optional LintFix is a suggestion the LSP turns into a QuickFix CodeAction (a new finding-keyed branch alongside the detector-driven SdlActions path, sharing only the WorkspaceEdit/TextEdit emit primitives); the build never mutates SDL. Fixes are registered explicitly, never divined from a deprecation’s prose reason, and are offered only where the edit is provably safe within the document: additive inserts (deprecations-have-a-reason, types-and-fields-have-descriptions), local renames offered only for undescribed fields (field-names-camel-case, no-typename-prefix, since graphql-java reports a described node’s location at the description, not the name token), and bare-form-only safe deletions for the two ignored-directive advisories (@record(record: {...}) has no computable end location, so it reports without a fix). Rename-class rules whose fix would ripple to references (type-names-pascal-case, input-object-name-suffix, enum-values-screaming-snake-case, input-and-argument-names-camel-case) ship no fix in v1. The pure graphql-java DeprecationRecognizer (the @deprecated docstring-token regex + native-marker read + typed DeprecationInfo{NATIVE, DOCSTRING}) is extracted down from graphitron-lsp into the graphitron build module so visitor 8 can consume it build-side; LspVocabulary now delegates and keeps its SchemaCoordinate adapter LSP-side (zero behaviour change, pinned by LspVocabularyTest/SdlActionDriftTest), and @record gains the docstring @deprecated marker so the convention is uniform. Coverage: pipeline-tier LintEngineTest (per-rule positive/negative/range + fix edit-range pins), LintRuleRegistryCoverageTest (every ENGINE rule registered exactly once, no CLASSIFIER advisory in the registry, subscribed ∪ not-linted partitions LintNodeKind with no overlap/gap, mirroring VariantCoverageTest/EdgeCoverageTest), ClassifierAdvisoryFixPipelineTest (emit-site fixes + bare-only guard), LSP-tier LintQuickFixTest + ValidatorDiagnosticsTest (build-side finding replays into a Warning squiggle at its range and applies to the corrected SDL, silenced on a stale snapshot per R139), and MCP-tier GraphitronMcpServerTest (both arms: no-rule advisory carries no lintRule, lint finding carries its id on the wire); findings asserted on the typed LintRule + SourceLocation, no code-string assertions on rendered diagnostic text or generated bodies. Deferred as designed: the plugin SPI, per-rule enable/disable + severity overrides + error-capable lint, a declarative rule-config DSL, reference-aware rename refactoring, and a second tree-sitter evaluator. Subsumes and retires R121 (redundant @splitQuery on @record) and R296 (deprecated-directive usage). Independent-session In Review → Done review; the full reactor is green under -Plocal-db once the local rewrite_test DB is seeded from the current init.sql and jOOQ regenerated (a stale sandbox catalog missing the R389 party joined-table fixtures reproduces the same Query.allParties/JoinedTableInheritancePipelineTest classification failures earlier Done reviews noted, on paths R398 does not touch). Approval corrected one false-invariant javadoc on LintNodeKind (it claimed a throw-on-unmapped instanceof chain the engine does not implement; the engine names the kind explicitly per dispatch site).

  • R409 (d2c8363 implementation, 9a22265 In Progress → In Review; Spec → Ready 972aeaf): Quiet the non-actionable mvn graphitron:dev RAG-warm startup log noise and document the recommended consumer .mvn/jvm.config for the warnings a plugin cannot un-print. A new RagLogQuieting helper lives in graphitron-mcp (where the RAG logger names are facts about the langchain4j-ONNX + Lucene dependency set R341/R372 dependency-quarantine, not knowledge the plugin’s compile surface should learn) and is called once from DevMojo.bindServer before the warms start() on the dev thread, so thread-start’s happens-before edge publishes the suppression to the graphitron-warm-* daemon threads that load the noisy classes (in-code comment forbids reordering it after start()). Group 1 (DJL HuggingFace tokenizer maxLength warning): non-actionable, no public knob, so muted defensively across providers, the slf4j-simple per-logger level property and the logger’s JUL level raised to SEVERE, so a Maven binding swap degrades to "noise returns" rather than a silent no-op; the DJL logger FQCN ai.djl.huggingface.tokenizers.HuggingFaceTokenizer confirmed against the 1.16.3-beta26 bge jar. Group 2 (Lucene VectorizationProvider incubator-module warning): actionable and directional, so demote-do-not-swallow, its JUL logger raised to SEVERE and, only when jdk.incubator.vector is absent (ModuleLayer.boot().findModule(...)), one concise graphitron-owned dev line names the --add-modules jdk.incubator.vector flag in place of Lucene’s multi-line dump; present module → silent (fast path already on). Group 3 (Maven-runtime jansi native-access / guava Unsafe warnings): documented only in getting-started’s new "Quieting startup warnings" note with a recommended .mvn/jvm.config, since the JVM prints them for Maven’s own lib/ jars before any plugin code runs. Helper javadoc names only what is attempted ("best-effort quieting"), never asserts the warning is gone, and pins the dev-goal-only scope (explicitly not shared with GenerateMojo/ValidateMojo, not triggered by the GraphitronMcpServer constructor, not to be hoisted). Coverage: unit-tier RagLogQuietingTest (Lucene JUL SEVERE; DJL slf4j-simple property error + its JUL SEVERE; calling twice a no-op; the incubator-hint decision as a pure function of module presence) ; 5 tests, no code-string assertions on generated bodies. Dev-tooling plumbing: no sealed variant, no classification, no emitted Java. Independent-session In Review → Done review; R409’s graphitron-mcp + graphitron-maven-plugin modules compile clean and RagLogQuietingTest is green, and the full-reactor graphitron-core failures (Query.allParties → UnclassifiedField; JoinedTableInheritancePipelineTest ClassCast) were confirmed to reproduce identically at the pre-R409 parent commit 5027e30, on classification paths R409 does not touch.

  • R261 (landing 6d5ce8b, Spec → Ready 7af72bf): Generation-time wire-coercion cast guard, Slice 1 (the three @service arg-classification sites). Before this, every arg-classification site fell through to CallSiteExtraction.Direct and emitted a raw (DeclaredType) wireValue cast that compiled cleanly and ClassCastException`d (or, for enums, `IllegalArgumentException`d) on the first request, since graphql-java delivers `ID/enum as String, Int as Integer, Float as Double, input-objects as Map. The fix homes the "coercion output assignable to declared type" verdict at the classifier in a new WireCoercionResolver (a sealed PassThrough | Rejected result, mirroring EnumMappingResolver.EnumValidation), consuming a new pure forward mapping ScalarTypeResolver.coercionOutputType (SDL scalar name → coercion-output TypeName, over spec built-ins, federation scalars, and classified @scalarType resolutions), keeping the verdict off ScalarTypeResolver (D1). A new WireCoercionError sub-seal of Rejection.AuthorError carries two arms on two axes (D4): Assignability (coercion class ≠ declared type; sites A-D) and EnumConstantDivergence (declared type is the enum but an SDL value name has no matching Java constant; site E), each with a stable lspCode() under graphitron.wire-coercion.* wired into Diagnostics.lspCodeOf, RejectionSeverityCoverageTest, and typed-rejection.adoc (+ drift list). ServiceCatalog.argExtraction (site B) is widened to take the resolved SDL leaf and return a sealed Resolved | Rejected; InputBeanResolver.bindField (sites A/E) calls the predicate on the scalar arm (widening R195’s jOOQ-record-only reject to the full wire-incompatible family) and routes the enum arm through EnumMappingResolver.checkEnumConstants, an extracted column-agnostic single parity home reused by validateEnumFilter (column path) and the @service enum producers (D3). Coverage: WireCoercionCastGuardPipelineTest asserts per arm on the typed rejection’s lspCode()/components (no code-string assertions on generated bodies), plus the ID→String, custom-@scalarType, and matching-enum non-regression cases that guard against over-rejection. Independent-session In Review → Done review; all R261-touched tests green and the whole graphitron-lsp module clean. Sites C (@condition) and D (@externalField) were carved into R411 (reject-wire-coercion-nonservice-sites, depends-on dimensional-model-pivot), which consumes this predicate unchanged; R261 dropped dimensional-model-pivot from depends-on and closes on the @service slice alone, with the @tableMethod/@condition caller deliberately left on ServiceCatalog.legacyArgExtraction until R411 threads the predicate through R222’s channel. The sandbox’s pre-existing Query.allParties / JoinedTableInheritancePipelineTest failures (missing PK metadata on the jti_* fixture catalog) were confirmed to fail identically at the pre-R261 parent commit 6ad75dd, so they are not an R261 regression. (Landing commit message misnames the carve-out item as "R407"; the sibling item, spec body, and README all correctly reference R411.)

  • R63 (landing 5ce8bc1, Spec → Ready 9ea2a8c): Lift the DML UPSERT/UPDATE dialect requirement off hand-built postDslGuard CodeBlock`s onto typed model data. A new sealed `DialectRequirement (None / RequiresFamily / RejectsFamily) plus a graphitron-internal SqlDialectFamily enum (a jOOQ SQLDialect.family() collapse; name-prefix fromDialectName covers the commercial-only ORACLE* / POSTGRESPLUS spellings the OSS jOOQ distribution omits) make the "UPSERT rejects Oracle" / "bulk UPDATE requires Postgres" facts discoverable on MutationField.DmlTableField.dialectRequirement() (never null), so the verb-neutral buildDmlFetcher skeleton stays verb-neutral and a future validator can read the constraint at validate time. Each of the four DML records carries the component, populated at its FieldBuilder construction site (UPSERT → RejectsFamily(ORACLE), bulk UPDATE → RequiresFamily(POSTGRES) selected on inputArg.list(), INSERT/DELETE/single-row UPDATE → None.INSTANCE); MappingsConstantNameDedup threads it through the error-channel rebuild. buildDmlFetcher’s `postDslGuard CodeBlock param becomes a DialectRequirement, collapsing three overloads into two, and a new emitDialectGuard helper renders the guard. Divergence from the Ready draft (documented in the spec body and landing commit, and verified in review): the draft emitted a reference to the generator-internal SqlDialectFamily enum into generated code, which does not compile in a consumer, where graphitron is test-scoped (graphitron-sakila-example/pom.xml) while the generated fetchers compile as the consumer’s main sources (GenerateMojo adds them via project.addCompileSourceRoot); the emitted guard instead stays self-contained, comparing jOOQ’s own dsl.dialect().family().name() against the family’s jooqFamilyName() (the reachable bulk-UPDATE output is byte-identical to the former inline family().name().equals("POSTGRES") guard, and jOOQ’s family() folds every ORACLE* spelling to ORACLE, so the UPSERT gate still catches them). fromDialectName is retained on the model for the future validator-time check. postInGuard, the sibling free-form CodeBlock carrying imperative per-row emission mechanics, stays a CodeBlock (documented non-goal: no DialectRequirement-shaped datum hides in it). Coverage: unit-tier SqlDialectFamilyTest (fromDialectName collapse + jooqFamilyName incl. the OTHER rejection), pipeline-tier DmlDialectRequirementClassificationTest (per-verb population), emitter TypeFetcherGeneratorTest (self-contained Oracle guard from a directly-constructed field + None emits nothing; UPSERT can’t classify through the pipeline under R144), and the bulk-UPDATE FetcherPipelineTest assertion re-anchored to the typed guard; no code-string assertions on generated bodies beyond the intentional guard-shape pins. Independent-session In Review → Done review; all R63-touched tests green (213), and the sandbox’s pre-existing Query.allParties / JoinedTableInheritancePipelineTest failures (missing PK metadata on the jti_* fixture catalog) confirmed to fail identically at the pre-R63 parent commit. Carries forward the R22 post-shipping follow-up.

  • R26: Umbrella tracker closed. Retiring graphitron-maven-plugin + graphitron-schema-transform into graphitron-rewrite shipped its build surface (schema loading, tagged inputs, Maven plugin, aggregator-standalone, content-idempotent writes) and @asConnection emit-time synthesis; the Java LSP rewrite + dev goal landed under R18; @notGenerated directive removal shipped on its own plan; Federation SDL integration continues under the separate Apollo Federation via federation-jvm transform backlog item; and the programmatic-schema architecture (Graphitron.buildSchema(...)) pruned type-extension merging, directive stripping, and client-SDL feature-flag splits from scope outright. Closed now rather than held open for its last sub-item, deleting graphitron-maven-plugin wholesale and unnesting the rewrite aggregator, which continues as its own item, R182 (unnest-rewrite-aggregator.md); an umbrella tracker has no reason to outlive the work it was scoping once every other bullet under it has landed.

  • R400 (Stage 1 0584430; Stage 2 page deletion 27412d3 + xref strip 2ea7900; In Review 4771bcd; rework fix 76d5ac9; spec/AC notes cff3120/2eca156; In Review 160ecef): Withhold the not-in-use directives from the v1 advertised directive surface, a docs-and-report-only trim with no generator behaviour change. DirectiveSupportReport.renderMigration (the roadmap-tool that generates docs/manual/_generated/supported-directives.adoc) gained two curated policy sets: REJECTED_ON_USE = {notGenerated, multitableReference} moved out of "Supported" into a new "Removed / rejected directives" section that tells migrating consumers to delete them, and WITHHELD_FROM_V1 = {tableMethod, sourceRow, experimental_constructType} silently excluded from "Supported" (declared and behaviourally unchanged, just outside the v1 surface). @record stays advertised as-is (deprecated + silently ignored, kept for v1 per the 2026-06-30 user decision). Stage 2 deleted the withheld trio’s dedicated reference pages (reference/directives/{tableMethod,sourceRow,experimental_constructType}.adoc) and the how-to/source-row.adoc recipe, and stripped every now-dangling xref and teaching passage across the index and recipe pages while keeping factual inline-code mentions (the directives are withheld, not removed); the AsciiDoctor fail-on-WARN render is the guardrail that no dangling xref survives. directives.graphqls is untouched and no classify-time rejection was added. Recovery is anchor-free (git log --diff-filter=D + checkout <commit>^ -- <path>) and ticketed under R403 (@tableMethod rethink + recover), R404 (@sourceRow recover), R69 (@experimental_constructType, gated on an emitter). Independent-session In Review → Done review across two cycles: the first requested rework because deleting the trio’s pages while keeping them declared broke R68’s DirectiveDocCoverageTest declared-directive ↔ reference-page bijection (a hard build failure); fixed by 76d5ac9, which narrows the invariant to "a directive needs a page only if it is on the advertised surface" and derives the exempt set from the generated supported-directives.adoc fragment (declared − mentioned = withheld) so the test cannot drift from the report that owns WITHHELD_FROM_V1. Coverage: DirectiveSupportReportTest (exclusion took effect: withheld trio + rejected pair absent from "Supported", rejected pair under "Removed / rejected", @record retained) and the carved-out DirectiveDocCoverageTest; full reactor green under -Plocal-db. Absorbs and supersedes the upstream "Remove the @tableMethod directive" proposal. Spawns R403/R404.

  • R389 (reshape a3899f4; classifier/emitter/party fixture 3586c06; pipeline tests 6aa4bb3; composite jti_* re-author d08eacb; rejection tests 521dd59; corpus + architecture-doc prose e163d59; In Review 5451c0f; self-review be47c31; schema-version rework 3dc0a3a): First-class discriminated joined-table (class-table) inheritance, where each participant declares its own detail @table distinct from the discriminated base and its inherited (base-resident) fields carry @reference back to the base. A new ParticipantRef.JoinedTableBound sealed variant carries the resolved child→parent JoinStep.FkJoin; residence is declared by the per-field @reference (base-resident → ColumnReferenceField, detail-resident → plain ColumnField) and read off the field variant, never recomputed, so no residence-aware resolver was added to FieldBuilder. TypeBuilder.buildParticipantList detects the detail-table participant, resolves the hop, skips the cross-table pass, and surfaces PK=FK / same-base / no-nameable-join violations as INVALID_SCHEMA diagnostics with candidate-FK hints. The interface fetcher selects FROM base with a per-participant discriminator-gated LEFT JOIN to each detail table (base-resident + shared-key fields off the base, detail-exclusive fields off the detail alias, NULL-through for non-matching rows); the same concrete type stays first-class standalone, resolving inherited fields through the parent reference. Both shared-key shapes ship: a new single-column party/party_individual/party_company fixture and the composite jti_* fixture re-authored to R389 (subsuming the R388 workaround). Coverage: @ExecutionTier allParties (routing + per-participant projection + NULL-through), allIndividuals (standalone), and the converted allSubjects composite cases against real PostgreSQL; @PipelineTier JoinedTableInheritancePipelineTest (positive shape, mixed discriminator-only + joined participant, three rejection invariants); a joined-table-interface R281 corpus example + code-generation-triggers.adoc prose; no code-string assertions on generated method bodies. Independent-session In Review → Done review across two cycles: the first requested rework for a missing jooq.codegen.schema.version bump (R389 changed init.sql but left it at 2.2, so jOOQ skipped catalog regeneration on incremental -Plocal-db builds → an UnclassifiedType cascade across 11 tests); fixed by 3dc0a3a (2.2 → 2.3) and verified with an incremental build that regenerates and is green. Builds on R388/R392; sets up R393 (base→detail join disambiguation).

  • R399 (In Review 75c4894; JSON-B switch e57bdbd; empty-body guard d6254dc): New graphitron-jakarta-rest module, a reusable spec-conformant GraphQL-over-HTTP serving library over a Graphitron schema, ending the four-dialect drift between the reference app and the tilgangsstyring-style consumer copies (each had diverged on media types, status codes, /schema, and GraphiQL). The dependency-inversion seam is a consumer-implemented GraphitronApplication SPI (schema(), newExecutionInput(), default engineBuilder(), default graphiqlEnabled()) plus an AbstractGraphitronApplication base that caches the schema from a supplier lambda over the generated facade, so the library never names a per-subgraph type. GraphqlResource (@Path("/graphql")) owns POST/GET content negotiation, the media-type-driven status watershed (modern application/graphql-response+json: unparseable → 400, malformed/validation/coercion → 422, executed → 200; legacy application/json always 200), GET mutation → 405, /graphql/schema via SchemaPrinter, and a CDN-based GraphiQL page; an application-scoped GraphqlEngine caches the built GraphQL. No custom JAX-RS providers: the resource reads the raw body and parses a GraphqlRequest record so it can shape parse errors as spec 4xx and own the status watershed. Two refinements from the signed-off spec, both sound: graphiqlEnabled() rides the SPI interface (callable through the injected reference, overridable by a direct implementor), and body marshalling uses the Jakarta JSON Binding (JSON-B) API rather than Jackson, leaving the library with zero concrete JSON dependency (consumer supplies the provider; Yasson / quarkus-jsonb) consistent with its all-jakarta.*-at-provided stance. The module is the first hand-written runtime artifact consumers depend on, a third Java-version category (rewrite-design-principles.adoc grew the bullet): it compiles at <release>17</release> (the Java-17 floor consumers share), is publishable, and joins the deploy set. graphitron-sakila-example refactored onto it (its GraphqlResource/GraphqlEngine copies and self-hosted GraphiQL assets deleted, replaced by a one-class SakilaGraphitronApplication adapter). Batching stays out of scope (the spec defines none). Coverage: an 11-case @ExecutionTier GraphQLOverHttpConformanceTest run through the reference app exercises the real library end-to-end, one citing case per committed normative requirement, each carrying the verbatim spec sentence + section + revision as a @DisplayName plus a requirement → section → test pointer table; the library itself carries no @Test classes by design (keeps the per-module tier-enforcement in-scope list from growing). No code-string assertions on generated bodies. The most spec-load-bearing, least-typed spot, statusFor’s `422-by-exclusion arm, is documented with the unit-tier escape hatch should a graphql-java upgrade ever add a pre-execution ErrorType. Independent-session In Review → Done review; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db (clean build, 2313 core tests + all 11 conformance cases, 0 failures; an initial red run traced to a stale sandbox rewrite_test DB missing R389’s party_* tables, resolved by a re-seed + clean rebuild, not a code defect in either item).

  • R401 (Half A 8a08759 + dispatch fix f225eeb; Half B e6cbe70; In Review e288bf4): Bundle the tree-sitter runtime in the natives jar so the LSP has zero native system dependency. no.sikt:graphitron-tree-sitter-natives now ships libtree-sitter alongside the grammar for all four supported platforms (eight lib/<os>-<arch>/ entries), built in CI from the pinned upstream v0.26.9 source tag (POSIX make; Windows MinGW-w64 with static-linked gcc runtime), gated on exported-symbol + transitive-dep allowlist assertions and a post-deploy-verify load+parse on all four platforms with no system libtree-sitter present; 0.26.9-1 published to Central by a human-dispatched release. BundledLibraryLookup extracts both binaries and returns grammar.or(bundledRuntime) (system probe deleted); GraphqlLanguage collapses the dual missing/too-old diagnostic apparatus (classifyInstalledRuntime, RuntimeStatus, runtimeProbePaths, tooOldRuntimeMessage, missingRuntimeMessage, ABI_VERSION_SYMBOL) to a single bundled-load-failure message naming the extracted temp path, with DOCS_URL repointed to the reference page. The lsp-requirements, getting-started #native-runtime-dependency, and reference-index docs collapse to "nothing to install" (per-platform install matrix and NixOS shell.nix snippet removed). Coverage: NativeLibraryBundleTest (per-platform @EnabledOnOs; linux-x86_64 loads the bundled runtime + grammar via the SPI and parses), TreeSitterSmokeTest, GraphqlLanguageErrorTranslationTest (the single-diagnostic + classifier + missing-path contract). Independent-session In Review → Done review (approval commit also corrected the natives pom.xml header comment and the UPSTREAM.md Windows build command, which still described the pre-bundle grammar-only module); full graphitron-lsp suite + reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db. Supersedes the system-dependency model from R203.

  • R269 (077f8f3 implementation; Spec → Ready fa6f38d, spec revisions 81ba814, spec refresh 1ac70fb): Null-guard the record-parent split-query accessor key extraction so a nullable to-one/to-many @table relation that resolves to no row renders null/[] instead of NPEing. GeneratorUtils.buildAccessorKeySingle (the ONE arm, the reported bug) read a nested jOOQ record off the parent backing via ((Backing) sourceExpr).accessor() and called .into(<PK columns>) on it with no null guard, so when a nullable to-one accessor returned null on an otherwise-successful parent the emitted fetcher threw Cannot invoke "...Record.into(...)" because "element" is null on the success arm rather than resolving the field; buildAccessorKeyMany had the analogous hazard one level out, the bare for-each over a never-populated to-many backing NPEing before any .into(...). The ONE arm now emits if (element == null) return CompletableFuture.completedFuture(null); between the accessor read and the key build, mirroring the FK-side buildKeyExtractionWithNullCheck precedent (a key that can’t match the terminal PK must not dispatch the loader; the to-one’s faithful "no row" rendering is null, and the fetcher’s CompletableFuture<DataFetcherResult<Record>> return makes completedFuture(null) assignable). The MANY arm took design fork (a): hoist the accessor result to a typed Iterable<Element> local and skip the for-loop when it is null so keys stays empty and the existing loadMany dispatch renders [] (the extraction block does not own its return path; both DataLoaderFetcherEmitter and MultiTablePolymorphicEmitter append their own dispatch, so skipping the loop is the consumer-agnostic shape). The asymmetry is deliberate and documented in both helpers: the ONE arm preserves null-vs-present (a to-one’s "no row" faithfully renders null) while the MANY arm collapses null-vs-empty (a to-many has no surface distinction between "never populated" and "zero rows" once the loader returns). Element-level nulls inside a populated collection stay unguarded (a malformed backing, not a cardinality to model). Emit-only: no model/classifier/AccessorRef/SourceKey change; producer/consumer linkage to the field’s nullability classification carried as a one-line rationale comment per the design-principles prescription (no validator invariant to mirror). The shared null key → completedFuture(null) seam across buildKeyExtractionWithNullCheck / buildAccessorKeySingle is named as a drift-prone follow-up, left out of scope as a refactor with its own blast radius. Coverage: execution-tier GraphQLQueryTest.inventoryById_filmCardDataNullAccessor_rendersFilmNullWithoutNpe (a FilmCardData accessor returning null for even film_id`s renders `film null with no error, odd film_id`s still resolve their `Film row through the same loader, the mixed-batch proof) and AccessorDerivedBatchKeyTest.accessorDerivedManyPayloads_nullToManyBacking_rendersEmptyListWithoutNpe (a CreateFilmsPayload with a null films() backing renders [], the present sibling still resolves); the RecordTableField + AccessorCall + Cardinality.ONE/MANY classifier shape the guards depend on is already pinned by GraphitronSchemaBuilderTest’s `AccessorDerivedSourceCase matrix; no code-string assertions on generated bodies. Split out of R268 (the error-arm short-circuit) and orthogonal to it; R271 had already retired the __elt/__k dunders. Independent-session In Review → Done review; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db (GraphQLQueryTest 286, AccessorDerivedBatchKeyTest 2, 0 failures).

  • R262 (c71885c, In Review c453aaf, Spec → Ready 498ddf7, Backlog → Spec d8ed14d): Reject @nodeId on non-ID coordinates and federation encoded @key fields at validate time. The SDL directive permits @nodeId on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION with no ID restriction, but every decode/encode arm is gated on "ID".equals(...), so a non-ID @nodeId was silently dropped and the raw base64 wire String bound undecoded: a green build with a production SQL bind/type error or never-matches predicate. Two build-time soundness reductions on the shared diagnostic channel (the R317-slice-5 / R204 / R194 pattern) close it. rejectNonIdNodeId(ctx) in GraphitronSchemaBuilder (sibling to rejectCaseInsensitiveTypeCollisions, same orchestration region) walks ctx.schema applied directives across all three on-locations, object/interface field definitions plus their arguments plus input-object fields, and registers an INVALID_SCHEMA ValidationError for any whose GraphQLTypeUtil.unwrapAll is not ID (findings F, G, and the output FIELD_DEFINITION encode mechanism that has no ID gate at all); it reads the raw schema rather than the registry precisely because a dropped @nodeId leaves no trace on the classified field. EntityResolutionBuilder rejects a federation @key whose referenced field resolves to a ChildField.ColumnReferenceField carrying CallSiteCompaction.NodeIdEncodeKeys (finding H, where the _entities DIRECT path would bind the encoded global id undecoded into the VALUES table) fatally via a new AltResult.Fatal arm carrying the typed Rejection.invalidSchema, distinct from and not co-located with the existing non-fatal compound-id BuildWarning; the column lookup is refactored from lookupColumn to columnOf(field) so the caller can inspect the field instance before reducing to ColumnRef. Decode-into-rep for encoded @key values, the cast-axis defects in wire-coercion-cast-guard, and consolidating the replicated @nodeId-site predicate stay out of scope. Coverage: pipeline-tier RejectNonIdNodeIdPipelineTest (6 tests, asserting the typed ValidationError coordinate + RejectionKind + message for the non-ID input field, argument, and output field plus the federation encoded-@key sub-case, and the legitimate ID input/argument/output coordinates and federation NODE_ID happy path that must keep passing); no code-string assertions on generated bodies. Independent-session In Review → Done review; RejectNonIdNodeIdPipelineTest green 6/6 in isolation under -Plocal-db. NB: the full reactor is currently red on three JoinedTableInheritancePipelineTest failures owned by R389 (an unrelated In-Progress item that landed after R262, 6aa4bb3); R262’s own delivery is independently green and those failures route through R389, not this item. Sibling of R397 (@error on bare-entity query fields) and R273 (NodeId mismatch semantics).

  • R378 (3ca8428; Spec → Ready e5dcd59, Backlog → Spec f146e1e): Authored @nodeId filters now throw on a malformed or wrong-type id instead of silently dropping it to the unfiltered baseline (the reported soknadId: ["IKKE_EN_ID"] returns-the-whole-table bug). The four authored filter producers (argument-level same-table and FK-target in FieldBuilder, input-object-field same-table and FK-target in BuildContext) flip from CallSiteExtraction.NodeIdDecodeKeys.SkipMismatchedElement to ThrowOnMismatch; the Relay heterogeneous-id-source pattern is given up deliberately per the user decision. CompositeDecodeHelperRegistry’s `Mode.THROW body is enriched once (list and scalar arms) with a two-branch message computed from NodeIdEncoder.peekTypeId on the offending wire value: peeked == null || expectedTypeId.equals(peeked) reads as structurally-malformed ("not a valid <Type> id"), any other non-null prefix as well-formed-wrong-type ("decodes to type <got>, expected a <Type> id"); the expected typeId is threaded onto HelperRef.Decode as a generation-time constant so the right-type-wrong-arity sub-case folds into the malformed branch. Error surfacing took path B (forward-compatible with a future query @error lift, R397): a generated <outputPackage>.schema.GraphitronClientException (subclass of graphql.GraphqlErrorException, so natively a GraphQLError, channel-matchable, serialisable into the errors array, serialVersionUID emitted for clean -Xlint:serial) is the stable client-error marker the THROW arm raises; new ErrorRouter.surfaceClientErrorOrRedact walks the cause chain and surfaces a GraphitronClientException’s real message while still redacting genuine internal faults to a correlation id, and the no-channel catch disposition is repointed at it uniformly across `TypeFetcherGenerator (renamed noChannelCatchArm), MultiTablePolymorphicEmitter, and ChannelCatchArmEmitter’s empty arm. Deliberate boundaries held: the two `__NODE_* synthesis-shim arms stay on SkipMismatchedElement (R273/shim-retirement track), and LookupValuesJoinEmitter’s separate N×M decode-throw site keeps its plain `GraphqlErrorException and still redacts (the R195/R315 record-decode boundary). No new validate-time rule (both arms and both registry modes were already fully implemented, so the flip introduces no unhandled classification; "validator mirrors classifier" satisfied vacuously). Coverage: execution-tier GraphQLQueryTest (malformed surfaces the real message naming the bad value + not a valid Film id; wrong-type surfaces decodes to type "FilmActor" / expected a Film id; mixed surfaces; input-object-field filter surface exercises the soknadId shape; a genuine internal fault still redacts to a correlation id, pinning the surface arm narrows to the client-error type; empty-list R375 baselines retained), pipeline-tier NodeIdPipelineTest / IdReferenceShimClassificationTest (authored arms pinned to ThrowOnMismatch, shim arms held at SkipMismatchedElement), unit-tier CompositeDecodeHelperRegistryTest + ErrorRouterClassGeneratorTest + GraphitronClientExceptionClassGeneratorTest (registry tests assert on code strings by that file’s existing convention; behaviour proven at the execution tier). Independent-session In Review → Done review; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db (GraphQLQueryTest 283, :graphitron 477 / :graphitron-sakila-example tiers all 0 failures). Orthogonal to R375; predecessor of R397.

  • R121, R296 (Backlog items, discarded as superseded by R398): the redundant-@splitQuery-on-@record LSP diagnostic (R121) and the deprecated-directive-usage BuildWarning (R296) are folded into R398’s starter lint-visitor set (visitors 9 and 8 respectively). Neither shipped a standalone implementation; their intent moves wholesale into the R398 SDL lint engine, which evaluates such rules build-side and projects them into the LSP. The build-tier @splitQuery warning R121 layered on already exists (FieldBuilder.warnIfSplitQueryOnRecordParent); R398 formalizes it as a visitor and adds the edit-time surface. IDs R121 and R296 are retired and not reused.

  • R395 (27325cd3e implementation, fixture-comment rework 7febb70b3; Spec → Ready 05e652893, Backlog → Spec bd0287f3e): Qualify the discriminated-interface discriminator column off the FROM table’s own jOOQ instance, not the @table(name:) directive string. R388 had changed the three discriminator SQL-emission sites in TypeFetcherGenerator (the __discriminator__ routing projection in buildInterfaceFieldsList, the ... IN (knownValues) restriction in buildDiscriminatorFilter, and the cross-table LEFT JOIN ON-clause gate in buildCrossTableJoinChain) to qualify via tableRef.tableName(), the verbatim case-preserved directive string. jOOQ renders the FROM table by its real schema-qualified, case-folded catalog name, so whenever the directive name differed in case or schema the qualifier did not match FROM and Postgres rejected the query with missing FROM-clause entry (reported against 10.0.0-RC21 by the opptak consumer: @table(name: "INNBOKS_MELDING") over kommunikasjon.innboks_melding). All three sites now emit <tableLocal>.getQualifiedName().append(DSL.name(col)) typed Object.class (so the .as / .in / .eq chains compile), producing the exact qualifier jOOQ renders in FROM by construction; tableRef.tableName() no longer reaches any discriminator site, and the read-side fourth site stayed correct (R392 had routed the TypeResolver off the synthetic __discriminator__ alias). The default-schema path does not over-qualify because the rewrite sakila tables sit in jOOQ’s unnamed default schema (getQualifiedName() contributes no schema part). Coverage: four regression-lock unit assertions in TypeFetcherGeneratorTest pin each site to the table-instance qualifier and forbid the directive-name string via a case-mismatched INTERFACE_BASE fixture (the case/schema-mismatch dimension lives here); a non-default-schema execution guard MultiSchemaQueryTest.signalsRouteToDiscriminatedTypesUnderNamedSchema over a new multischema_a.signal @discriminate interface (AlertSignal carrying a cross-table @reference to widget, NoticeSignal) covers the dimension R388 regressed on; the default-schema guards PolymorphicProjectionQueryTest and GraphQLQueryTest allContent / allSubjects confirm no over-qualification. Documented deviation: the execution fixture uses the unqualified @table(name: "signal") rather than the spec’s schema-qualified / upper-case form, which a separate @reference FK-connection check (filed as R396) rejects; the unqualified directive still renders FROM as "multischema_a"."signal" and so still fires the pre-fix bug, matching the reported consumer shape. In Review → Done reviewed by a session distinct from the implementer; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db.

  • R386 (c68b000 implementation; Spec → Ready 270b0c1, Backlog → Spec 3c6430b, filed 27d9c9e): catalog.search MCP tool (R118 slice 10) ; the semantic counterpart to the structured catalog.tables / catalog.describe, giving an MCP-aware agent fuzzy natural-language discovery over the database catalog ("where are customer addresses stored?") so a developer pointing graphitron at a large existing schema finds tables without knowing their SQL names. CatalogDescriptors is a pure, ONNX-free composer that turns each R362 CatalogFacts.Table into one readable descriptor carrying both the raw SQL token (so BM25 matches film_actor exactly) and its normalized words (a state-machine splitWords splitting snake_case / camelCase / acronym / digit runs ; customerID → "customer id", IDColumn → "id column", address2 → "address 2" ; the model-agnostic retrieval lift of R118 OQ3), degrading to names-only when jOOQ captured no comments (OQ4); the SHA-256 corpusHash is length-prefixed over the exact descriptor strings handed to embedDocuments, so the hashed thing and the embedded thing cannot drift. CatalogSearchIndex owns the warm-managed, self-observing Lucene index, mirroring R374’s ReverseEdgeIndex.Cache (no BuildArtifacts component, no Workspace field, no DevMojo listener): each search reads the live catalogFacts through two gates ; reference identity (the cheap common path), then content hash (a no-op recompile that swaps the reference but not the content re-embeds nothing) ; and a changed hash kicks an AsyncWarm re-embed off the classpath-watcher thread, re-entering the existing WarmState.Warming shape (no new "refreshing" state) so the prior Ready index keeps serving while the new one builds. The index persists under ${project.build.directory}/graphitron-mcp-rag/catalog/<corpusHash>/ as a Lucene FSDirectory (survives dev restarts, dies on mvn clean), with an embedder-identity manifest (getClass().getName() + dimension()) written beside each index so loading a future multilingual-e5-small index under the English bge embedder (both 384-dim, indistinguishable by dimension alone) is rejected and rebuilt rather than silently mis-served (closes the R118 OQ2 cross-model trap now); sibling hash dirs are reaped keeping the current plus one prior. The tool takes query (required) + limit (default 10), returns {status, results:[{id, schema, name, comment?, score}]} by the same schema-qualified SQL id catalog.describe accepts (discovery hands off to description), and returns the shared WarmState.degradationMessage + {status: warming|failed} while the index is not Ready. Threaded through GraphitronMcpServer’s widened five-arg constructor (RagConfig` as a growable record, back-compat overloads default it to a temp dir) and DevMojo.bindServer (supplying the build-dir cache root); the multilingual swap is flagged, not done. Coverage: unit-tier CatalogDescriptorsTest (split normalization cases, comment-present vs name-only degradation, raw+normalized tokens, length-prefixed hash stability/segmentation) and CatalogSearchIndexTest over FakeEmbedder + FSDirectory (hash-gated re-embed via an embed-call-count spy, warming-on-change re-entry, embedder-identity rejection-then-acceptance, persistence round-trip + current-plus-one-prior reaping, cross-warm Failed propagation), MCP-handler-tier GraphitronMcpServerTest (catalog.search advertised in tools/list; a ready-arm call returns ranked schema.table ids whose top feeds a follow-on catalog.describe; the warming-arm call returns status: warming ; structured-content assertions only), and infrastructure-tier @Tag("slow") CatalogSearchOnnxTest (real bge ONNX embeds a Sakila-shaped CatalogFacts, asserts public.address / public.payment rank for natural-language queries ; the retrieval-quality + normalization payoff pin); no code-string assertions on generated bodies. getting-started.adoc gains the agent-facing tool note (semantic search, the warming/refresh behaviour, the jOOQ-comment-capture lift). Builds on R372 (RAG foundation: Embedder + the Lucene EmbeddingStore), R362 (CatalogFacts + the schema-qualified ids), and R341/R361 (the graphitron-mcp module + the shared-model / dev-trigger seam); sibling of R385 (docs.search, slice 9). In Review → Done reviewed by a session distinct from the implementer; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db.

  • R392 (d832aaa, In Review flip 2a808d4): Route the discriminated single-table interface (@table + @discriminate) TypeResolver off a synthetic discriminator alias instead of the raw column name. R388 qualified the three SQL emission sites but left a fourth, read-side site: the generated TypeResolver read the discriminator with a bare record.get(DSL.field(DSL.name(col))). When the interface also exposes the discriminator as a queryable field, the participant $fields projects the real catalog column too, so the result carries the discriminator twice (the two-part routing add and the three-part schema-qualified column); the bare read matches both and jOOQ logs Ambiguous match found, resolving to the first by luck. Fix mirrors the multi-table __typename convention: a shared MultiTablePolymorphicEmitter.DISCRIMINATOR_COLUMN = "__discriminator__" constant (declared with its collision rationale, reaching generated code only as string literals ; a .as("__discriminator__") projection and a record.get(DSL.name(...)) read), projected under that alias in TypeFetcherGenerator.buildInterfaceFieldsList and read back in GraphitronSchemaClassGenerator, so routing is unambiguous and the user-facing discriminator field still resolves from its own column. The WHERE filter and LEFT JOIN ON-clause keep referencing the real qualified column (unaffected). Coverage: execution-tier GraphQLQueryTest.allSubjects_discriminatorFieldInsideFragment_routesViaSyntheticAlias (discriminator field selected inside the inline fragment; asserts routing per type plus the as "__discriminator__" projection via SQL_LOG) and the schema-generator unit test build_typeResolver_routesOffSyntheticDiscriminatorAlias (alias read replaces the raw-column read, per that class’s documented runtime-dispatch-infrastructure body-content exception). Residual hole left by R388; first-class per-participant @table joined-table inheritance remains R389. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R383 (6df5616 implementation, In Review flip 0770918; Spec → Ready 0657369): Support nested-input @field filters on multitable interface/union root queries. R363 lowered @field-mapped filter inputs onto multitable interface/union fields but scoped day one to the branch-safe top-level extractions (Direct / EnumValueOf / ContextArg), so the idiomatic input-object shape (occupants(filter: OccupantFilter) with OccupantFilter { firstNames: [String!] @field(...) }) lowered to a NestedInputField(filter -> firstNames, leaf) call-site extraction that FieldBuilder.firstUnsupportedFilterArg rejected at classify time with an author-error, even though the leaf is a plain scalar; consumers hit it as a hard build failure on an ordinary filter-input schema. The fix is a classifier relaxation only, no registry / lift-context plumbing: FieldBuilder.isBranchSafeExtraction becomes a recursive switch where a NestedInputField is branch-safe exactly when its leaf is (Direct / EnumValueOf / ContextArg admitted; a NodeIdDecodeKeys leaf stays rejected through the recursion, a developer @condition stays rejected by the GeneratedConditionFilter guard), and the switch is exhaustive over the sealed CallSiteExtraction with no default (the five non-branch-safe permits listed as explicit false arms) so R384 lifting one fails to compile and forces a deliberate decision at this gate. The "this list-typed nested leaf extracts as (List<X>) map.get(key), an unchecked cast" fact is lifted onto the model as CallParam.emitsUncheckedCast() (single source of truth, Generation-thinking); both hosts (MultiTablePolymorphicEmitter’s `buildMainFetcher / buildRootConnectionFetcher and the single-table QueryConditionsGenerator) fold over their call params and ask the model rather than each re-deriving list() && instanceof NestedInputField, so R384 adds its unchecked-emitting arm in one place and neither host can drift. The condition-method generator is extraction-agnostic, so the generated <Participant>Conditions method is byte-identical whether the value arrives top-level or Map-traversed. Coverage: pipeline-tier MultiTableFilterLoweringTest (nestedInputFieldFilter_lowersPerParticipantWithNestedExtraction asserts the NestedInputField(filter -> firstNames, Direct) call param per participant; nestedInputFieldCondition_rejectedStructuralNotDeferred keeps a nested-input developer @condition a structural author error) and execution-tier MultiTableFilterExecutionTest over AddressOccupant = Customer | Staff (occupantsByFilter(filter: { firstNames: [...] }) filters per branch and returns one matching row each, an empty filter narrows by nothing), with a new sakila OccupantFilter input + Query.occupantsByFilter fixture; no code-string assertions on generated bodies. The converted (JooqConvert / ID-typed), @nodeId-decoded, and developer-@condition kinds remain deferred to R384, which carries the registry / FK-target-alias plumbing they need. Builds on R363; sibling of R384. In Review → Done reviewed by a session distinct from the implementer; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db.

  • R390 (5485578, In Review flip 92e7e16): Retain the connection carrier element subgraph in the rebuilt assembled schema. A @table type reachable only through a directive-driven @asConnection carrier was pruned by SchemaTransformer once ConnectionPromoter.rebuildAssembledForConnections retyped the bare-list carrier to name its synthesised Connection: the element type then hangs off the Connection’s nodes / Edge’s node GraphQLTypeReference`s, which the transform treats as leaves, so an element reachable nowhere else (a nested-only chain), and its whole transitive subgraph, dropped out, either silently (its `<Type>Type schema class never emitted, the consumer’s RC20 cannot find symbol javac failure) or as an NPE in the type-reference resolver when a surviving typeRef still pointed at it; because the element was never traversed, a nested carrier’s own @asConnection rewrite was also skipped, leaving that field a bare list while its fetcher was connection-shaped (the report’s variant-3 mismatch). Fix pins each rewritten carrier’s element type (resolved off the pre-rewrite schema by carrierElementType, null-degrading when the parent/field/base type can’t be found) as a GraphQLSchema.additionalType before the transform, deduped by name alongside the synthesised types; every carrier at every nesting depth is in the rewrite set, so pinning the direct carrier elements keeps each element and its concretely-reachable subgraph alive and every nested rewrite applies. Structural (SDL-declared) connections are untouched (no typeRef introduced, element stays concretely referenced). Coverage: pipeline-tier NestedConnectionElementRetentionPipelineTest (2: a Store --@asConnection--> Customer --@reference--> Payment nested-reference chain retains both element types in the rebuilt assembled schema; a --@asConnection--> nested chain retypes Customer.payments to CustomerPaymentsConnection! rather than leaving a bare list, with the synthesised ConnectionType classified), structural assertions only, no code-string assertions on generated bodies. In Review → Done reviewed by a session distinct from the implementer; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db.

  • R385 (b0afcf8 implementation, 6cb322a README regen, d751110 rework, In Review flip ee71c81; Spec → Ready 5cdcbe5, Spec 2f014ce, filed 27d9c9e): docs.search MCP tool (R118 slice 9) ; build-time .adoc chunking + a pre-embedded bundled index, async-loaded at startup for semantic retrieval over the public manual, and the first slice to wire R372’s async-warm lifecycle into the running server + DevMojo. AdocChunker is a pure (adoc, sourcePath) -> List<DocChunk> that walks raw .adoc heading syntax (no AsciiDoctor render, so it stays off the docs module’s JRuby cost), splitting on section boundaries with an opt-in // rag:split override, fenced-block awareness (== inside a ---- listing is body, not a heading), explicit [[anchor]] override, and a heading-path breadcrumb prepended to each chunk’s embed text so a passage keeps its context. DocsIndexBuilder (bound to process-classes via exec-maven-plugin, reading the public manual under a declared <docs.source.dir>) chunks + embeds via BgeEmbedder.embedDocuments and writes the bundle to target/classes/mcp/docs-index/, gated by a SHA-256 content-hash stamp so an unchanged inner loop skips the ONNX cost. Bundle divergence (settled at Spec): rather than a literal Lucene FSDirectory (unreadable from a jar, would force temp-dir extraction), DocsBundle packages pre-embedded (id, embedText, payload, vector) tuples behind a (magic, version, dimension, count) header with explicit byte-length string prefixes (past the writeUTF 64 KB ceiling); the docs warm rebuilds an in-memory LuceneEmbeddingStore.inMemory(dimension) by re-add()-ing the tuples, re-embedding nothing at runtime. The store-opaque payload is a dependency-free URL-safe Base64 encoding (not the spec’s literal "payloadJson") ; a JSON parser would widen the module’s quarantined RAG dependency surface for a string only this module produces and consumes; this is the slice’s one principled divergence and is documented inline. DocsSearchTool embeds the query (embedder.embedQuery), runs the EmbeddingStore seam, and returns ranked passages with headingPath / sourcePath / anchor / text / score / a https://graphitron.sikt.no/... deep link; a once-memoised dimension guard reconciles the runtime embedder against the bundle’s build-time width and degrades cleanly on skew rather than throwing an opaque Lucene KNN error; either warm absent or not-Ready returns the shared WarmState.degradationMessage and no hits, leaving the dev loop structured-only. Wired through GraphitronMcpServer’s new structured-only / injected-warm two-constructor seam and owned by `DevMojo. Rework (returned to Ready at df7525a, re-landed at d751110): the first In Review pass failed the gate on a red build ; DevMojoTest paid a real BgeEmbedder ONNX load that SIGSEGV’d the surefire fork, and the MCP-bind-failure catch arm leaked the warms it had started. Fixed with a package-private warm-factory seam on DevMojo (mirroring the server’s injected-warm pattern; mojoFor defaults to structured-only null warms, the bind-failure test swaps in ONNX-free fakes) and an awaitAndCloseWarms() that joins each warm to its terminal state and closes the warmed docs store on the bind-failure unwind before rethrowing. Coverage: unit-tier AdocChunkerTest (5: nested heading paths, // rag:split override, malformed-rag:-comment near-miss is body not a split, fenced-block heading-syntax is body, explicit anchor override), DocsBundleTest (3: write→read count/ids/dimension/vector-width round-trip, separator-collision-safe payload decode, header-only readDimension), pipeline-tier DocsRagWarmPathTest (the production loadDocsIndex loader end-to-end), MCP-handler-tier GraphitronMcpServerTest (warming/failed/ready/dimension-mismatch cases, structured-content assertions), and DevMojoTest (the bind-failure unwind leaves no live warm and freed the docs store); the build-time ONNX embed itself stays out of the fast suite (R372’s @Tag("slow") BgeEmbedderOnnxTest covers the real load). getting-started.adoc gains a paragraph. Builds on R372 (RAG foundation) and R341/R361 (the graphitron-mcp module + live-Workspace seam); sibling of R386 (catalog.search, slice 10). In Review → Done reviewed by a session distinct from both implementer sessions; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db (DevMojoTest 6, GraphitronMcpServerTest 37, AdocChunkerTest 5, DocsBundleTest 3, DocsRagWarmPathTest 1; no SIGSEGV).

  • R379 (6f68611; Spec → Ready 713d464, spec revise 07c7dd3, Spec/filing db865ef/1d8a78f): Validate that an @reference path’s joins compile, moving two javac-in-generated-code failure modes to build-time classification. Check 1 (terminal hop lands on the return table): InlineTableFieldEmitter.buildArm feeds the terminal hop’s alias to a $fields overload typed for the field return type’s @table, so a terminal hop landing elsewhere (the NusGrupperingFagfelt @table("NUSFAGFELT") reached via a path ending on NUSFAGGRUPPE reproduction, found in a downstream subgraph build) compiled to an incompatible-types error in a consumer’s build. BuildContext.parsePath now computes a typed TerminalTargetVerdict (Match / Mismatch(fieldName, terminalTableName, returnTableName) / NotApplicable) over R232’s already-resolved terminal JoinStep.HasTargetTable.targetTable() and threads it onto ParsedPath, never re-deriving the hop kind from the directive element; the Mismatch diagnostic is formatted from the record’s fields so message and projection cannot drift. Check 2 (condition-method parameter tables): a two-argument condition method (a ConditionJoin ON clause or an FkJoin.whereFilter) emitted positionally as method(sourceAlias, targetAlias) that concretely types a jOOQ Table parameter must match the alias the emitter passes it; validateConditionParamTables / checkConcreteParamTable check parameter 0 against the hop source and parameter 1 against the hop target at each condition resolution site, skipping the idiomatic wildcard Table<?> signature (unverifiable, fully accepted) and routing mismatches through the existing errors → Rejection.AuthorError.Structural channel. Deviation (scope correction, agreed at review): the spec’s draft said to self-reject inside parsePath, but parsePath is shared by callers (@tableMethod, @nodeId, RecordTableField) that carry their own terminal-target checks (FieldBuilder.java:4692/:5778); self-rejecting there would preempt them and re-introduce the Generation-thinking same-predicate-two-consumers smell the item cites. Check 1 instead threads the typed verdict and rejects only at the two inline output callers (TableBoundReturnType, TableInterfaceType in FieldBuilder), the sole emit shape carrying the $fields(terminalAlias) invariant, double-gated on non-null start (excludes @sourceRow) and non-null return table (excludes input-field sites). The verdict is also the typed hook R381 Slice B consumes. Coverage: pipeline-tier ReferencePathTerminalTargetTest (7: terminal {table:} / {key:} / multi-hop landing on the wrong table rejected with pointed diagnostics, happy-path mirrors, a pre-existing mid-path-disconnect regression fence) and ReferencePathConditionParamTest (6: both carriers and both parameter positions rejected, wildcard (Table<?>, Table<?>) and matching-concrete happy paths) over deliberately-mistyped TestConditionStub fixtures, message-content assertions only, no code-string assertions on generated bodies; no emitter behaviour change. In Review → Done reviewed by a session distinct from the implementer; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db.

  • R363 (2ede6e4 implementation, 73bc057 self-review scope-tightening; Spec → Ready 66cb717, spec revises 6a3076d/50a4db9/413753a): Lower @field-mapped filter inputs onto root multitable interface/union query fields, closing a data-correctness leak where a filtered slice was requested but the rewrite emitted a bare UNION ALL and returned every row. The filter surface is per participant, not a single shared list: the same logical @field filter resolves to a different table-specific WhereFilter per participant (e.g. FeideApplikasjonConditions.… vs MaskinportenApplikasjonConditions.…), so a new field-local model/ParticipantFilters record pairs each ParticipantRef.TableBound with the filters lowered against its own table; the carrier is not a component on the type-scoped shared ParticipantRef, and the two fields stay off SqlGeneratingField (their return type is PolymorphicReturnType, a sibling of the TableBoundReturnType the capability’s returnType() requires, so implements would not compile). FieldBuilder lowers once per table-bound participant via resolveTableFieldComponents against the participant table with a participant-named conditions class (participant.typeName(), not elementTypeName, so the per-participant methods do not collide), surfaces any participant’s Rejected (absent or type-incompatible column) as the field’s rejection, and dedupes the @asConnection same-table advisory across the N participant calls. @condition (field- or argument-level) is rejected before the per-participant loop with a non-deferred Rejection.structural (no dangling planSlug), since resolveTableFieldComponents itself lowers a @condition bound to whatever table it is handed; guarding after the loop would pin the developer’s single-table method to the wrong table on N-1 branches. MultiTablePolymorphicEmitter threads a typename-keyed filter map into both branch loops, ANDing each participant’s predicate into its stage1_<Type> branch WHERE (buildStage1Block combines it with the existing parent-FK predicate; buildStage1ConnectionBlock gains a per-branch WHERE it never emitted before); TypeConditionsGenerator is wired to the polymorphic fields' participantFilters since they are not SqlGeneratingField. The self-review pass narrowed day-one extraction scope to the branch-safe Direct / EnumValueOf / ContextArg kinds: FieldBuilder.firstUnsupportedFilterArg structurally rejects JooqConvert (deprecated-for-removal DataType.convert trips the consumer -Werror), NodeIdDecodeKeys (needs the CompositeDecodeHelperRegistry), and nested-input / developer @condition filters (the classifier-guarantees-emitter-assumptions floor that lets branchFilterWhere drop all registry/alias plumbing), with R383 filed for lifting the rest. orderBy is split to R382 (__sort__ doubles as the connection cursor seek key). Coverage: pipeline-tier MultiTableFilterLoweringTest (per-participant lowering for interface + union; absent-column rejection; ID-typed JooqConvert rejected structural; field- and arg-level @condition rejected structural, not deferred) and execution-tier MultiTableFilterExecutionTest (AddressOccupant = Customer | Staff, list + @asConnection forms filter per branch and return only matching rows, exercising both buildStage1Block and buildStage1ConnectionBlock); no code-string assertions on generated bodies. Shares MultiTablePolymorphicEmitter with R365/R366/R367. In Review → Done reviewed by a session distinct from the implementer; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db.

  • R374 (5193fdc; Spec → Ready 44b0146, Backlog → Spec fbb6c1f, filed b106ccc): MCP cross-reference edges (R118 slice 7): the traversal layer over the frozen R362/R368 structured tools, all module-local to graphitron-mcp. A new edges tool (D-A, a dedicated tool rather than a neighbours field retrofitted onto every result, so the eight existing contracts stay frozen) takes exactly one node selector (field / type / table / column`+table` / method / class) plus a direction (out / in / both) and returns that node’s typed neighbours. D-D: a sealed NodeRef model (TypeNode / FieldNode / TableNode / ColumnNode / MethodNode / ClassNode) owns the whole stable-ID grammar and composes each wire string only at the McpWire boundary (methodRef / new columnId), reconciling the classifier’s bare table names through CatalogFacts.resolve (the qualified schema.table IDs catalog.describe accepts) and its arity-free (class, name) method pairs through the external-reference scan (one RESOLVES edge per overload). D-B: an EdgeKind label enum (BACKS / TARGETS / REFERENCES / RESOLVES / PARTICIPATES) with the varying endpoint shape pushed entirely into NodeRef.target, so the enum carries no kind-dependent nullability (the sealed-over-enum tension resolved); the TARGETS / REFERENCES split falls out of the classifier’s joinPath distinction rather than being re-derived. The arm-to-kind mapping is an exhaustive no-default switch over every FieldClassification (28) and TypeClassification (23) permit, mirroring SchemaView ; the cross-module drift guard: a new classifier permit fails the edge switch to compile. D-C: a lazy, (snapshot, catalogFacts)-reference-pair-memoised ReverseEdgeIndex (the slice’s real deliverable, for impact analysis: which schema fields bind a given column / method / table), built by inverting the same per-field switch the forward producer uses so the two directions cannot disagree, holding no new BuildArtifacts / Workspace field. Stage 3 (neighborhood subgraph tool) and indexing the forward-walkable PARTICIPATES (type → type) direction correctly deferred per R118 OQ6. Deviation from the spec’s mcp/edges/ sub-package: the edge model stays in the flat mcp package to reuse McpWire’s package-private grammar composers, single-sourcing the wire grammar rather than widening `McpWire’s deliberately-internal surface ; judged sound (it serves the same boundary-encoding principle D-D leans on). Coverage: MCP-handler-tier `GraphitronMcpServerTest (forward Column / ColumnReference-with-joinPath / table-bound ServiceBacked / @node-type; reverse column / method / table directions asserting the endpoint slot holds the field not the queried node; ambiguous / notFound / two-overload fan-out reconciliation; Unavailable-before-build and memo-rebuild-on-build-swap), structured-content assertions only, plus unit-tier EdgeCoverageTest partitioning every permit into edge-bearing / no-edge with overlap / missing / stale guards (the live drift-guard pin). In Review → Done reviewed by a session distinct from the implementer; graphitron-mcp module suite (52) and all 35 R374 tests green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db (one full-reactor run flaked on the unrelated R372 BgeEmbedderOnnxTest strict-margin similarity assertion, which passes in isolation and on a focused module run; untouched by R374).

  • R377 (b44de9b; Spec → Ready 2afdec9, Backlog → Spec 212f21c, filed 26e82a0): decode<typeId> mismatch when multiple @table types share a table. BuildContext.resolveDecodeHelperForTable resolved through findGraphQLTypeForTable, an all-@table index that counts nesting-projection types and so returns empty (ambiguous) for any table backed by more than one object type; that routed decode resolution to a typeId-named fallback (decode<typeId>), which agrees with NodeIdEncoderClassGenerator’s emitted `decode<TypeName> only when typeId equals the type name. A customized numeric @node(typeId:) over such a table (a @node plus a nesting-projection @table, e.g. UTDANNINGSMULIGHET) emitted a decode<typeId> call javac could not resolve, a latent error that surfaced only in the consumer’s compile, found porting utdanningsregisteret to Graphitron 10. Fix: rewrite the resolver to the @node-only NodeIndex by-table view (nodes.forTable), which is exactly the right domain (it sees only @node types, not the projection types sharing the rows), with a three-way outcome: one node → its decodeMethod() (type-name keyed, matching the encoder); two or more → null, which the four callers already map to a validate-time "zero or multiple GraphQL types map to it" rejection rather than a phantom decode<typeId> (validator mirrors classifier invariants); no node → the orphan-input typeId fallback (synthesis-shim retirement track, retire-synthesis-shims.md). The wrong-domain branch 1 is dropped entirely rather than masked behind branch ordering; findGraphQLTypeForTable stays for its one remaining caller (the id-reference synthesis shim). The casing divergence is closed structurally: NodeIndex.byTable is keyed on the lowercased @table(name:) echo at construction (TypeBuilder.buildClassificationIndices) and NodeIndex.forTable lowercases its lookup arg, so a consumer never re-establishes the TableRef.sameTable contract. Coverage: three pipeline-tier NodeIdPipelineTest cases (decode-via-index-not-typeId asserting decodeSharedNode not decode10154; multi-node rejection to UnclassifiedType with the "zero or multiple" message; orphan-input typeId fallback pinning the branch-1-drop decision) over a new nodeidfixture.shared_node table with a customized numeric typeId, plus a graphitron-sakila-example compilation backstop (FilmEndorsementNode @node(typeId: "920534") over film_endorsement, already backed by the FilmEndorsement projection, decoded by a new endorsementsByNodeId query) so the javac-stage failure is caught end-to-end by the module’s <release>17</release> compile; structural assertions only, no code-string assertions on generated bodies. In Review → Done reviewed by a session distinct from the implementer; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db.

  • R375 (8e0887a code, fe4303d plan-body test-name fixup; Spec → Ready 0147443, Backlog → Spec 4b443e9, filed b804f4c): Empty list passed to a fetch-path list-IN filter now narrows by nothing (DSL.noCondition() identity) instead of emitting IN (), which jOOQ renders as the constant false and silently zeroed the query. An external bug report (10.0.0-RC18, regression from 9.3.0) hit this through Apollo Client serialising an empty selection as [] on a list @nodeId filter argument: the empty list AND-ed an unsatisfiable predicate into the WHERE and dropped every row. The rewrite has no hasIds branch (R50/e4b collapsed list @nodeId filters without @condition onto a plain column-shaped BodyParam.In/RowIn), so the symptom was general to every list-IN filter, not @nodeId-specific. Fix: a literal empty guard on all four In/RowIn arms of TypeConditionsGenerator.buildConditionMethod (non-null if (!arg.isEmpty()); nullable folds the emptiness into its existing != null check); Eq/RowEq unchanged (scalars have no empty state). The fetch/lookup split is principled, not a per-field carve-out: on a fetch field a list filter is an optional narrowing predicate whose empty identity is noCondition (the list-arity sibling of the null/omitted case R230 already drops), while a lookup field’s input rows are the FROM-side of a VALUES…JOIN (LookupValuesJoinEmitter), where empty is an empty join domain and 0 rows is the only coherent answer; TypeConditionsGenerator already excludes LookupField upstream (line 63). The guard is emitted as a literal, not lifted into a sealed EmptyBehavior model slot: within this emitter "drop on empty" is a constant invariant with a single value, so the Generation-thinking two-consumer trigger does not fire and a single-case sub-taxonomy would be over-engineering (the DML-consumer lift point is named for if one ever appears). Scope item surfaced during implementation: filmsByNodeIdArg (argument-level same-table @nodeId, R106-lifted onto the WHERE film_id IN (...) rail) is a fetch field, so its empty and all-malformed execution tests carried stale pre-R106 lookup wording and asserted the empty set only because IN () = false coincidentally zeroed the query; both inverted to the unfiltered baseline. The all-malformed case (SkipMismatchedElement drops every id → empty List<Integer>, indistinguishable from a literal [] at the condition method) follows the same noCondition rule per the wire-format-boundary principle (decode classifies skip-vs-throw at the boundary; downstream sees tuples with no provenance), and whether all-malformed should instead surface a user error is correctly split out to R378 (decode strictness, Backlog). Coverage: pipeline-tier TypeConditionsGeneratorTest (!ids.isEmpty() assertions on the In and RowIn arms + new inFilter_nonNullList_emitsEmptyGuardWithoutNullCheck pinning the non-null arm the nodeId helpers don’t reach), execution-tier GraphQLQueryTest (inverted films_filteredBySameTableNodeId_emptyListReturnsUnfilteredBaseline, new connection regression filmsConnectionByOptionalIds_idsEmptyList_paginatesFullTableAndCountsAll asserting nodes + totalCount both unfiltered, inverted filmsByNodeIdArg_{emptyList,allMalformedIds}_returnsUnfilteredBaseline, and the lookup-divergence comment on inlineLookupTableField_emptyInput_returnsEmpty). In Review → Done reviewed by a session distinct from the implementer; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db (GraphQLQueryTest 275, full suite 461 + 65, 0 failures).

  • R376 (64c6f23 implementation, 35a2e7e stale-javadoc cleanup; Spec → Ready f3f2a26, spec refinements e60d49b 52fc227, filed 21c4e62): Goto-definition (and the declaration-name hover overlay) on a method-backed SDL field name now jumps to the bound Java method, not just the column / accessor / record-component cases that resolve through the enclosing type’s backing. A @service / @externalField / @tableMethod field (and its root query/mutation forms) previously had no field-name jump at all: the cursor had to be parked on the directive’s method: / className: argument before Definitions would navigate. The bound class+method were already resolved on the snapshot’s FieldClassification; goto and hover simply never consulted it for the name trigger. Routing: a new DeclTarget.methodBackedTarget arm consults built.fieldClassification(parentType, member) before the TypeBackingShape dispatch in ofField, so a method-backed classification (six variants: ServiceBacked, Computed, TableMethod, QueryService, QueryTableMethod, MutationService) takes precedence over the parent table’s column backing; it rides the already-projected snapshot (no source-index read in the pure core), keeping the R371 goto/hover structural parity intact. Arity, primary with a name-level floor: DeclTarget.SourceMethod widened from (class, accessorMethodName) to (class, methodName, paramCount) so both consumers key the source index on the same overload, retiring the hover overlay’s arity-0 hardcode (correct only for zero-arg POJO accessors; a service method takes at least a DSLContext, which would have made goto jump while hover returned empty, violating overlayIsPresentExactlyWhenGotoJumps). SourceWalker.Index gains a never-dropped methodsByName view and resolveMethod: the precise (class, name, arity) key first (lands on the correct overload), falling back to the name-level view when that key is absent or was dropped as a same-arity collision, so a same-arity overload still lands on a declaration adjacent to the set rather than declining. DefinitionTarget.Ambiguous is retired: the directive-arg Definitions.methodTarget path is aligned to the same floor (no Ambiguous non-jump remains anywhere on the navigation path, the spec’s open-question fold-in). Two documented, safe-degrading assumptions: the method-backed arm uses default -> empty over the 32-variant FieldClassification rather than an exhaustive switch (a future method-backed variant silently won’t extend, scoped out by the spec forbidding a FieldClassification model change), and keys the lookup on the resolved member name, relying on method-backed fields carrying no @field(name:) override (a miss degrades to the prior no-jump, never a wrong jump); for an arity-overloaded service name the classification records no signature, so resolve() takes the first catalog candidate’s arity and the name floor still guarantees a jump. Spans graphitron-lsp plus the small SourceWalker.Index addition in graphitron. Coverage: DeclarationDefinitionsTest (the four named variants jump end-to-end; an arity-distinguishable greet() vs greet(String,int) resolves to the correct overload, not the name floor; a same-arity collision still jumps via the floor; classified-but-unindexed returns empty), DefinitionsTest (the former Ambiguous case now falls back to a name-level jump), DeclarationHoverOverlayParityTest (a non-zero-arity SourceMethod parity case). In Review → Done reviewed by a session distinct from the implementer; full reactor green under mvn -f graphitron-rewrite/pom.xml install -Plocal-db (graphitron-lsp suite + 461 + 65, 0 failures).

  • R372 (c2acec1; Spec → Ready 5dadff3, Backlog → Spec c154ce2, filed 47e4e24): MCP RAG foundation (R118 slice 8): the semantic-layer infrastructure the docs.search (slice 9) and catalog.search (slice 10) tools sit on, all module-local under graphitron-mcp/.../rag/, registering no agent-facing tool and leaving GraphitronMcpServer untouched. Three seams plus one lifecycle. D1: a graphitron-owned Embedder seam (embedQuery / embedDocuments / dimension) that owns the bge query/document asymmetry ; BgeEmbedder prepends the bge instruction prefix on the query path only ; and whose Query / Embedding records bundle BM25 text with its KNN vector and name no langchain4j type, so the multilingual swap (R118 OQ2) attaches to this wrapper, not the library; English-only bge-small-en-v1.5-q (384-dim) for V0. D2: an EmbeddingStore seam with LuceneEmbeddingStore (BM25 + KNN in one index, fused by reciprocal-rank fusion) as the sole shipping backend, an in-RAM ByteBuffersDirectory instance of the same class as the seam’s test fake, and the dimension invariant checked once at add against the embedder’s dimension(). D3: a generic sealed WarmState<T> over Warming / Ready / Failed with a handle-agnostic degradation-message helper (exhaustive switch, no default, rejects Ready), and the AsyncWarm<T> background-daemon harness whose await() returns the terminal value (never Warming) so a dependent build-warm maps an upstream Failed into its own Failed rather than hanging; volatile state read mirrors R361’s per-field posture. Dependency quarantine (R341): the one genuinely heavy native dependency (ONNX Runtime JNI, pulled transitively by the bge module) plus Lucene core land on graphitron-mcp alone, never the plugin’s compile surface; surefire on this module gains --enable-native-access=ALL-UNNAMED with no excludedGroups, so CI’s default mvn verify -Plocal-db runs everything. Coverage: seam-tier (fast, no ONNX) asymmetry routing, store KNN round-trip + BM25 hybrid surfacing a lexical match, dimension guard, load-only-rejects-add, WarmState transitions across both type parameters + await propagation / cross-warm failure; infrastructure-tier BgeEmbedderOnnxTest (@Tag("slow"), runs in CI) loads the real bge model asserting dimension 384 and a strict-margin similarity separation. In Review → Done reviewed by a session distinct from the implementer; full reactor green under -Plocal-db (RAG seam + ONNX tests all run, 0 skipped). Blocks slices 9/10/11.

  • R362 (ea47993 implementation; Spec → Ready 173a7d0, Backlog → Spec 304c39f, filed d7c8d15): MCP catalog.tables / catalog.describe over a build-time CatalogFacts projection (R118 slice 2, on the R361 seam). Resolves R361 D1 to build-time enrichment (option A, not a retained loader): a new frozen CatalogFacts record (tables keyed by schema-qualified SQL name; columns with SQL+Java names, SQL types, nullability, comments; PK / unique keys; indexes; in/out FKs with their column pairs) is built once per catalog rebuild in CatalogBuilder.buildCatalogFacts(JooqCatalog) while the codegen loader is open, carried as a third BuildArtifacts component beside CompletionData and the snapshot, and swapped onto a volatile Workspace.catalogFacts field in setBuildOutput. The load-bearing invariant (no Table<?> / ForeignKey<?,?> / Field / Class retained) is what lets the projection outlive the per-pass withCodegenScope loader close; new resolved-immutable JooqCatalog accessors (allTableEntries, candidateKeys(Table<?>), columnFactsOf, indexFactsOf, foreignKeyFactsOf) reduce every live handle to String at the parse boundary. GraphitronMcpServer registers the two tools mirroring the R361 statusTool shape: catalog.tables (schema + SQL-name-substring filters, opaque base64-offset cursor paging with nextCursor) and catalog.describe (resolved / ambiguous / not-found arms over a parallel CatalogFacts.TableResolution). sqlType maps to the jOOQ DataType.getTypeName() (SQL discovery key, not the Java FQN); no new classifier or validator branch (a read-only projection of already-classified facts). Coverage: pipeline-tier fact capture over the real Sakila catalog (film columns/PK/index/in+out FKs with column pairs, storage_bin unique key distinct from PK) plus a structural recursive no-live-handle walk and a close-then-read smoke test; MCP handler tests driving a real loopback server assert mapped structuredContent for list/filter/page and describe resolved/ambiguous/not-found. In Review → Done reviewed by a session distinct from the implementer; mvn install -Plocal-db green (CatalogFactsTest 9, GraphitronMcpServerTest 11, full suite 460 + 65).

  • R365 (3366bbe floor, 976e0a3 classifier+emitter, 0e82f71 execution fixture; In Review → Ready rework e7477ac, rework fix b28fe15, mutation fixture + drop-contract fd1f59e; Backlog → Spec b6a6f93, Spec → Ready 0426d42): Restore the graphitron 9.3 ability to return a polymorphic entity from a root @service field (route (a): the service hands back a PK-populated jOOQ TableRecord per branch, and the generated fetcher dispatches on each returned record’s runtime class to pick the participant, tags __typename, and auto-fetches the selected columns by PK). ServiceDirectiveResolver.projectReturnType’s `PolymorphicReturnType arm now resolves to a new Resolved.Polymorphic success (the all-@error errors-channel lift still takes precedence) instead of the old "not yet supported" reject; new QueryServicePolymorphicField / MutationServicePolymorphicField leaves carry the participant set and service method, and MultiTablePolymorphicEmitter.emitServiceMethods reuses the multitable query path’s stage-2 by-PK auto-fetch (buildPerTypenameSelect) verbatim, replacing stage-1 UNION-ALL discovery with record-class dispatch over the returned records. Scope is exactly one shape: a @service returning a distinct-table multitable interface. Three guards keep that floor honest at the one shared validateMultiTableParticipants site and at classify: same-table participants in a plain multitable interface/union are an AUTHOR_ERROR (record-class dispatch cannot tell shared-recordClass participants apart, with or without @discriminator; model as a single-table discriminated TableInterfaceType or split) ; this also guards R363’s query path; a @service returning a union is permanently unsupported (AUTHOR_ERROR, union polymorphism is a generated-query-path capability the service path never grew); a @service returning a single-table discriminated interface (TableInterfaceType) is deferred-rejected (the table-bound service path emits no per-row discriminator dispatch). Child @service polymorphic returns stay deferred (root only). The In Review pass requested rework on a silent-misdispatch hole (same-table participants carrying @discriminator passed the original no-discriminator-only floor and reached a dead-arm instanceof chain in the emitter, contradicting "validator mirrors classifier invariants"); the rework closed it by rejecting both same-table subsets and narrowing scope to interface-only. Route (b) (the { field, errors } payload + errors: envelope shape) remains a separate follow-up on R366/R367. Coverage: pipeline-tier corpus (query-service-polymorphic, mutation-service-polymorphic) + a @ProjectionFor projection test, unit-tier floor pins in QueryInterfaceFieldValidationTest / QueryUnionFieldValidationTest (both same-table subsets → AUTHOR_ERROR), builder negatives (serviceReturningUnion_rejectedAsUnsupported, serviceReturningTableInterface_deferred), and execution-tier ServicePolymorphicReturnExecutionTest (query single + list and a @service mutation list round-trip, each dispatching two distinct-table branches by runtime record class against real PostgreSQL); the route (a) drop contract (a returned record matching no participant or no live PK row is dropped) is documented in buildServiceMainFetcher. Shares MultiTablePolymorphicEmitter with R363/R366/R367. In Review → Done reviewed by a session distinct from the implementer (the independent reviewer requested the rework and verified the green re-run). graphitron (full suite) and graphitron-sakila-example (460 tests) green under -Plocal-db; graphitron-lsp / graphitron-mcp not exercised at review (tree-sitter native runtime unavailable in the agent sandbox, orthogonal to this change which touches no LSP/MCP files).

  • R367 (208a3e5, 629960b, 162a7fd; Backlog → Spec b6a6f93, Spec → Ready 88e1659, In Review → Ready rework cb2b4b9): Single-cardinality polymorphic child on a record-backed (Pojo / JavaRecord) parent, closing the capability gap the generator deferred at FieldBuilder’s `!fieldIsList arm (whose Rejection.deferred(planSlug: "polymorphic-child-record-parent-single-cardinality") pointed at a roadmap doc that never existed, a dead link in the generator’s own diagnostic). MultiTablePolymorphicEmitter.buildScalarPerParentFetcher gains a record-parent arm: it now takes parentSourceKey and, for a Reader.AccessorCall parent, binds parentRecord to the accessor’s returned hub TableRecord (((Backing) env.getSource()).<accessor>()) instead of casting the source to a jOOQ Record (which would ClassCastException on a Pojo source); a null hub yields a null payload, and the table-backed arm keeps the (Record) env.getSource() cast. FieldBuilder.resolvePolymorphicRecordParent drops the deferral so both cardinalities route through derivePolymorphicHubSource, removing the only reference to the dangling slug. Scope is top-level backing classes; the nested-backing-class Outer$Nested non-compiling-cast hazard (shared with the list arm via ClassName.bestGuess over a binary name) is filed as R370. Coverage: the two pipeline-tier deferral assertions in RecordParentMultiTablePolymorphicPipelineTest flip to assert successful AccessorKeyedSingle classification (interface + union), and execution-tier AddressOccupantCarrierSingleCardinalityTest drives a Pojo carrier holding an AddressRecord hub through Query.addressOccupantCarrier, pinning firstOccupant to the first Customer|Staff by sort order (Staff for a populated address) and to null over an occupant-free hub. The rework (162a7fd) fixed a red build the first In Review pass shipped: the no-occupants case queried addressOccupantCarrier(addressId: 4) but init.sql seeded only addresses 1-3, so a fresh DB returned a null carrier and the assertion failed (masked locally by a polluted persistent native DB; reproduced on a clean DB and in CI run 28104926051); the fix seeds an occupant-free address 4 (district Tasmania, no store/staff/customer) so the test exercises the empty-stage-1 null-payload arm over a non-null carrier rather than the null-carrier short-circuit, and corrects a stale SDL comment naming a nonexistent nested AddressOccupantCarrierService.Carrier. Sibling of R366 (list cardinality); enables R365 shape (b); shares MultiTablePolymorphicEmitter with R363. In Review → Done reviewed by a session distinct from the implementer (independent reviewer requested the rework and verified the green re-run). Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 12 modules).

  • R369 (1a8b57d; Backlog → Spec 87544d0, Spec → Ready bf002a5, Ready → In Progress acef1bc): graphitron:dev walks generated-sources of scanned reactor modules so goto-definition / hover reaches jOOQ tables that live in a separate module from the schema module running dev. Root cause was a lifecycle asymmetry between the two root sets AbstractRewriteMojo feeds the LSP: resolveClasspathRoots() reads target/classes (on disk from any prior build, so every reactor sibling is scanned for completion), but resolveCompileSourceRoots() read project.getCompileSourceRoots(), which only carries a generated-sources root once that module’s codegen plugin ran in this session, so a sibling jOOQ module unbuilt this session contributed zero walked source roots and its @table / @field jumps landed on DefinitionTarget.SourceAbsent (silent no-jump), breaking the R351 scan/walk parity invariant. Fix is in the shared resolver, not a dev-only branch (D2): new generatedSourceRoots(MavenProject) does a lifecycle-independent disk scan of the existing immediate subdirs of target/generated-sources/ (the generated-sources/<tool> convention every generator follows, not POM-config parsing ; D1), and compileSourceRootsOf(MavenProject) is the single per-module "what is walked" definition (getCompileSourceRoots() ∪ generatedSourceRoots) that both resolveCompileSourceRoots() and the new unwalkedScannedModules(Iterable<MavenProject>) route through, so resolver and diagnostic cannot drift; collectExistingDirs dedups by normalised absolute path so a root the plugin already registered (full-lifecycle goals) collapses, making the widening a no-op there. DevMojo renders a startup WARN naming any residual scanned-but-unwalked module (the dependency-JAR case with no .java to walk, explicitly out of scope for the auto-include). The stale resolveCompileSourceRoots() javadoc was rewritten to match. Coverage: unit-tier AbstractRewriteMojoTest (5: disk discovery incl. stray-file exclusion, empty cases, the core widening regression, plugin-registered dedup, `+unwalkedScannedModules` reporting only the genuinely-unwalked module); unit-tier SourceWalkerTest.disjointGeneratorPackagesKeepTableJumpLocatedNotAmbiguous pins the D1 output-package-disjointness argument (a graphitron output root walked alongside a jOOQ root leaves the table-class jump Located, not Ambiguous) rather than leaving it as unpinned prose; no generated-body string assertions. Builds on R351 (parity invariant + collectExistingDirs) and R352 / R90 (SourceWalker.Index, DefinitionTarget empty-resolution contract). In Review → Done reviewed by a session distinct from the implementer. graphitron and graphitron-maven-plugin test tiers green under -Plocal-db (AbstractRewriteMojoTest 9/9, DevMojoTest 6/6, SourceWalkerTest 9/9); the graphitron-lsp / DevServerTest completion-socket tests could not run in the agent sandbox (the tree-sitter native runtime install clones GitHub, which the egress policy denies ; orthogonal to this change, per the spec’s verification caveat).

  • R361 (d08b6ed; Backlog → Spec 8d0f8e0, Spec → Ready 60e0667): MCP shared-model seam, slice 1 of the R118 MCP programme. The R341 skeleton served static content only (prompts capability alone, an argument-less about prompt) and held no reference to the live generator model. R361 widens GraphitronMcpServer’s constructor to `(InetSocketAddress, Workspace) and holds the live handle; DevMojo.bindServer passes the same Workspace instance it hands the LSP DevServer, so the existing schema / classpath / source watchers refresh it in place with no new trigger, listener, or refresh path (the volatile fields give per-field visibility on the next read). The tools capability is declared (.tools(false), the listChanged boolean) with one liveness status tool that reads Workspace.snapshot() on every call and reports the snapshot on its two orthogonal axes, availability (Built / Unavailable) and freshness (Current / Previous, absent when unavailable), mapped through an exhaustive switch over the LspSchemaSnapshot sealed permits with no default so a new arm forces a compile-time choice rather than silently flattening; no domain counts (those are the later structured-tool slices' wire contracts). D1 keeps the seam Workspace-only: the raw JooqCatalog is not threaded (it reflects lazily against the codegenLoader URLClassLoader that withCodegenScope closes each pass), deferred to slice 2. Adds the acyclic graphitron-mcp → graphitron-lsp compile edge (plugin → {graphitron, lsp, mcp}; mcp → lsp → graphitron), orthogonal to the module’s native-RAG dependency quarantine. Coverage: infrastructure-tier GraphitronMcpServerTest boots a real server and drives it with the real MCP client, asserting tools/list advertises the status tool and tools/call returns the two-axis snapshot on both the default Unavailable arm and a setBuildOutput-driven Built.Current arm; DevMojoTest’s bind-failure unwind retargeted to the widened constructor; no code-string assertions. Builds on R341. In Review → Done reviewed by a session distinct from the implementer. Full reactor green under `-Plocal-db.

  • R341 (df99ed9; spec lifecycle 115d174, 56e8fb6, b271054, 07755b5, Spec → Ready 83f0542): MCP server skeleton embedded in graphitron:dev. The smallest useful Model Context Protocol server that gives an MCP-aware agent (Claude Code, Cursor) ambient context about a graphitron project and, the load-bearing reason, establishes the transport-and-lifecycle seam the live catalog/schema discovery tools (R118) build on; it serves static content only. New graphitron-mcp module (GraphitronMcpServer implements AutoCloseable) hosts the MCP Java SDK 2.0.0 servlet-based Streamable HTTP transport (HttpServletStreamableServerTransportProvider) in embedded Jetty 12 EE10, bound loopback-only on 127.0.0.1:8488 (the LSP’s is 8487) at the /mcp endpoint, serving the handshake instructions string plus a single argument-less about prompt, both read once at startup from bundled jar resources (mcp/instructions.txt, mcp/about.md) mirroring LspVocabulary’s shape-not-state posture. The dedicated module is the dependency-quarantine seam that keeps R118’s heavy native deps off `graphitron-maven-plugin’s own compile surface (the "Separate business logic from API code" axis the `graphitron-lsp split also serves, not transport symmetry); it is published like graphitron-lsp (the plugin declares a compile-scope dependency and a Maven plugin resolves its declared deps from the consumer’s repositories at execution time), so it carries no maven.deploy.skip. Lifecycle wiring: DevMojo.bindServer constructs the server as a sibling of the LSP DevServer and DevMojo.cleanup closes it; mcpPort defaults to DEFAULT_MCP_PORT = 8488 but is deliberately not a @Parameter (a configurable port stays deferred). A taken MCP port fails fast with a MojoExecutionException naming the conflict (the fail-fast diagnostic promoted out of Deferred for parity with the LSP bind, per "Stability through simplicity") and closes the already-bound LSP socket so a partial bind leaks nothing. The startup log names the MCP URL and a copy-pasteable claude mcp add line; graphitron-sakila-example ships a committed .mcp.json. Coverage (transport-glue layer, outside the four-tier classifier/emitter enforcement by design): infrastructure-tier GraphitronMcpServerTest (3, boots a real server on an ephemeral port and drives it with the SDK’s own client: initialize carries the bundled instructions; about is advertised argument-less and returns the explainer; a taken port throws IOException); DevMojoTest gains the Mojo-message + no-LSP-leak case and a DEFAULT_MCP_PORT == 8488 pin on defaultsMatchPlanContract. Docs: getting-started.adoc gains a user MCP subsection and the contributor "how this is wired" section grows from four to five cooperating components (+ mermaid node, Ctrl+C cleanup); README.adoc module count 9 → 10 and graphitron-mcp added to the publishable surface. Out of scope and staying in R118: live catalog/schema tools over the warm Workspace, docs RAG, any vector store / embeddings / ONNX, the stdio-to-HTTP proxy, and the decision on keeping R118’s heavy deps off the non-dev plugin goals. In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 12 modules).

  • R358 (58bc29b, 7202e20; spec 61d5a9c, Spec → Ready 2ac746d, revise Ready → Spec 2ad3d45, Spec → Ready d75c838, Ready → In Progress e2e9612, In Progress → In Review e23b203): Guard table-name comparisons against case-sensitivity drift. TableRef.tableName() is the case-preserved verbatim @table(name:) echo, so the same logical table can surface as two differently-cased strings (the verbatim @table casing vs the lowercase jOOQ Table.getName() the record-class resolution path feeds in), and a case-sensitive .equals silently mis-decides under an Oracle-style UPPERCASE @table over a lowercase jOOQ catalog: the R357 bug, which FieldBuilder.resolveCarrierIdEncoder (:3105) was one explicit @nodeId(typeName:) hop from reproducing. Two phases. Phase 1 (58bc29b): convert the :3105 @nodeId(typeName:) NodeType-vs-carrier comparison to case-insensitive, and add TableNameComparisonCaseGuardTest (@UnitTier), a recursive source scan over src/main/java/no/sikt/graphitron/rewrite forbidding the .tableName().equals( spelling with a nonzero-scanned-file tripwire against a vacuous pass. Phase 2 (7202e20): move the comparison onto the type as TableRef.sameTable(String) / denotesSameTableAs(TableRef) (case-insensitive, null-safe canonical identity; tableName() stays the verbatim diagnostic echo), migrate all ~10 comparison sites across FieldBuilder / TypeBuilder / GraphitronSchemaValidator / BuildContext / NodeIdLeafResolver (both operand orientations, behaviour-preserving), and strengthen the guard to forbid every raw tableName() comparison (both .equals / .equalsIgnoreCase, both orientations) excluding the predicate’s home model/TableRef.java. The guard is then a backstop on a predicate correct by construction, the "model carries what the consumer needs" principle (rewrite-design-principles.adoc:17: the same predicate evaluated by multiple consumers is a sign the resolver is under-specified). Coverage: pipeline-tier MutationDmlNodeIdClassificationTest#bulkDeleteIdCarrier_explicitNodeId_caseMismatchedTable_admits (explicit @nodeId(typeName: "Bar") with UPPERCASE @table(name: "BAR") NodeType over lowercase @table(name: "bar") carrier; the spec’s :3105 reachability obligation, option (b)), verified rejecting pre-fix (the carrier drops to a non-SingleRecordIdFieldFromReturning classification) and admitting post-fix (encodeBar wired, empty diagnostics); unit-tier TableRefSameTablePredicateTest (matching/mismatched casing both directions, null arg); the strengthened TableNameComparisonCaseGuardTest as the structural net, verified tripping on a planted raw comparison with a named-site message; no generated-body string assertions. Out of scope and filed as R359 (d1985ec, column-sqlname-comparison-case-guard): the structurally identical ColumnRef.sqlName() sibling (one live .sqlName().equals( at GraphitronSchemaValidator.java:883 alongside six equalsIgnoreCase sites). Canonicalizing tableName() at construction rejected because it would change author-facing diagnostic casing (a documented invariant). Depends-on R357 (which converts FieldBuilder.java:5114; R358 owns :3105 and the structural pin). In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db).

  • R357 (faa465b; roadmap add 3b7d54a, Backlog → Spec 8d8cb45, Spec → Ready fdad51d): Case-insensitive @table(name:) match in record-composite carrier accessor resolution. A @service record-composite carrier (R329’s two-level Payload { results: [Result], errors } / Result { @table children } shape) whose result-type @table children declare @table(name:) in a case differing from the lowercase jOOQ catalog name misclassified every such child as UnclassifiedField with the three-option resolveRecordParentSource author error. collectAccessorMatches (FieldBuilder) grounds the DTO and resolves the accessor plus its element table correctly, then dropped the match on a single case-sensitive element-table guard: expectedSqlName carries the verbatim @table(name:) casing (via resolveTable) while the accessor’s element TableRef carries the jOOQ Table.getName() casing (via resolveTableByRecordClass), and the two diverge only when SDL casing differs from catalog casing (the driving utdanningsregisteret schema writes UPPERCASE @table(name:) against lowercase Postgres). The one-line fix aligns that comparison to equalsIgnoreCase, the table-name idiom already used at eight other sites. Surfaced by the utdanningsregisteret Graphitron 10 migration. Coverage: pipeline-tier ServiceRecordCompositeCarrierPipelineTest#caseMismatchedTableName_classifiesCompositeChildrenAsRecordTableField (the R329 FilmWithActors carrier with @table(name: "FILM") / @table(name: "ACTOR") against lowercase film / actor, asserting both children classify as RecordTableField ONE/MANY with empty diagnostics, the verdict not the case-insensitivity mechanism), verified failing pre-fix (both fall to UnclassifiedField) and passing post-fix; no generated-body string assertions. Out of scope and filed as R358 (table-name-comparison-case-guard): the sibling .tableName().equals( at FieldBuilder.java:3105 (R358 carries the conversion and is re-examining whether it is a latent instance of the same bug rather than inert) and a unit-tier guard pinning the idiom; canonicalizing TableRef.tableName() rejected because it would change author-facing diagnostic casing (a documented invariant). In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R351 (bdabb1e, ffb2589; In Progress → In Review 59e1cc3; absorbs R352): Complete the LSP goto-definition decoupling R349 began, so the jOOQ half rides the source-cadence index too and both halves resolve through one shape. Source-root parity made structural: AbstractRewriteMojo’s scan path (resolveClasspathRoots) and walk path (resolveCompileSourceRoots) collapse onto one package-private, unit-tested `collectExistingDirs traversal over the same reactor project set, so a class scanned for completion provably has its source root walked for goto-definition; DevMojo logs a startup classpath-root / source-root / external-reference count so the "completion works but goto-definition returns nothing" report self-diagnoses. jOOQ half on the source index: CompletionData.Table / Column / Reference drop SourceLocation and instead carry the generated table-class FQN (and the Keys-class FQN for references); Definitions joins those FQNs against the LSP-owned SourceWalker.Index at request time and routes the @table / @field / @reference arms through the same exhaustive DefinitionTarget switch the service half uses, retiring the file-head 0:0 synthesis (a known table whose source is not on a walked root lands on SourceAbsent, a clean non-jump). Hover / description onto the source cadence: CatalogBuilder no longer walks .java at all (descriptions are the build-derivable fallback only: the jOOQ table’s SQL comment, empty for columns and services); a new Descriptions overlay reads the source-derived Javadoc from the index at request time with per-element precedence (table SQL-comment wins; column / class / method source Javadoc wins), and Hovers plus the FieldCompletions / TableCompletions detail read through it, so hover and goto cannot show two snapshots of one declaration mid-edit. Static cache → instance: SourceWalker’s per-file cache moves from a process-wide static onto an instance owned by `Workspace alongside the volatile sourceIndex; Workspace.refreshSourceIndex is the single walk entry point, called by the dev goal’s source-root watcher. Coverage favours real end-to-end over mocks: SourceCadenceHoverAndDefinitionTest walks real .java through the real Workspace / SourceWalker / Hovers / Definitions (asserting hover and goto move together across a source edit with the catalog the same instance, no rebuild); CatalogBuilderSourceTest inverts to pin the build-boundary decoupling (documented sources on the build are not lifted); SourceWalkerTest pins per-instance cache isolation (a same-mtime content change a static path+mtime cache would mis-serve); AbstractRewriteMojoTest pins the collectExistingDirs classpath/source-root parity; DefinitionsTest reaches every jOOQ arm and every DefinitionTarget outcome; no code-string assertions on generated method bodies. In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db). Predecessor R349 (the service half and the DefinitionTarget typing); absorbed R352 (the jOOQ-half / hover-cadence / static-cache follow-up R349 deferred).

  • R342 (fa2743f; spec e736a03, Spec → Ready de7490b): Structural dedup + value agreement for bulk UPDATE SET columns written by overlapping carriers. The fourth and last mutation write surface left open by R322 (the bulk UPDATE t SET c = v.c FROM (VALUES …) AS v(…) path) and the lift of R354’s deferred bulk self-FK form. The bulk path walked its SET groups per-group with no cross-group column dedup, so two writers landing on one backing column emitted that column twice in the derived table’s v(…) list and crashed loud (a duplicate-column Postgres/jOOQ error, not a silent drop, which is why R322 deferred it here). Two shapes were affected: a decode-involving within-SET overlap (a plain @field plus a @nodeId FK reference whose lifted child column coincides), and a self-FK @nodeId on a list-input UPDATE (rejected at validate time by UpdateRowsWalker Stage 2b). Plan: TypeFetcherGenerator.setColumnPlan(List<SetGroup>), the SET analogue of insertColumnPlan, groups the set groups' columns by backing-column sqlName into an ordered writer list (each carrying its source SetGroup index, slot, and ColumnRef), shared() when ≥2 writers; the three bulk emitters (emitSetVColNameAdds, emitSetBulkCellAdds, emitSetVFieldPuts) all walk the one deterministic plan, so the v(…) column-name list, the per-row cells, and the sets.put entries emit exactly one entry per distinct column and cannot drift out of positional alignment. Presence gate (setColumnPresenceGate): a disjoint column keeps its single writer’s first-row gate byte-identical to the pre-dedup form; a shared column’s gate is the disjunction of its contributing writers' first-row presence, and the uniform-shape guard makes projecting row 0’s disjunction onto every row safe (a gate keyed on the wrong writer would silently drop a present shared column, the exact silent-drop class R322 closed on the other surfaces; pinned by the two asymmetric-presence execution tests). Cells: the per-row decode locals are hoisted once per row into emitBulkSetDecodeLocals (INSERT-style instanceof String guard + presence-gated throw), so a composite group’s cells and a shared column’s gather all read one decode rather than re-decoding per writer; a shared column gathers the present writers' values (reusing R354’s presence-guarded appendAgreementValue), pairwise-checks them through NodeIdEncoder.requireColumnAgreement, and adds the single coalesced DSL.val(firstPresent, col.getDataType()) cell, emitInsertAgreementPrep’s coalesced-cell shape transplanted into the row loop (no `DSL.defaultValue branch, since the conditional gate guarantees a present writer, and not emitSetAgreementPreamble’s check-then-let-the-puts-run shape, which the bulk derived table’s lack of a last-write-wins `Map.put affordance forbids). Cross-partition (WHERE∩SET self-FK) fork (resolved in In Progress with principles-architect): setColumnPlan sees only SET groups, so a self-FK’s column shared with the WHERE identity is handled outside it, the two v-populating emitters skip a SET column already supplied as a WHERE/lookup v-column, emitSetVFieldPuts keeps the no-op sets.put (keeps sets non-empty so the empty-SET runtime guard does not fire on a minimal self-FK input), and a new emitBulkKeySetAgreement emits the per-row check reusing the already-present per-row decode locals (bulkKey<gi> WHERE-side, bulkSetKey_<gi> SET-side) rather than re-decoding (two decodes/row, not three; the agreement guards the values actually used). Walker: Stage 2b’s bulk self-FK UnsupportedInputFieldShape reject is deleted and the now-dead list parameter dropped from walk (both FieldBuilder call sites + the class javadoc updated); the walk is cardinality-independent, since R354 already routed a self-FK all-SET regardless of the list flag, so removing Stage 2b exposes a shape the classifier already routes correctly. UnsupportedInputFieldShape retains its other producers. Coverage: UpdateRowsWalkerTest (15 pass: the bulk self-FK reject test inverted to admit-and-route-all-SET with the shared mailbox_id in both partitions, plus a decode-involving-overlap-admits-without-PlainColumnCollision test); execution-tier BulkUpdateSetAgreementExecutionTest (7 tests: within-SET agree / disagree-rolls-back / asymmetric-present ×2 on the film_endorsement updateEndorsementsOverlap fixture, self-FK agree-repoints-with-mailbox_id-no-op / disagree-rolls-back / omitted-nullable on the email updateEmailReplies fixture), the list-input siblings of NodeIdValueAgreementExecutionTest / SelfFkNodeIdUpdateExecutionTest; compilation tier via the two new schema fixtures against real jOOQ at Java 17; no generated-body string assertions. Out of scope: the shared overlap-analysis abstraction lift across the now-six instantiation sites (R356, filed alongside this spec, depends on it); non-Postgres dialects; any change to the single-row UPDATE SET / INSERT / @service paths (R322/R354). In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R355 (6562b7a; Backlog → Spec 8133ecf, Spec → Ready 42efe84): Infer depth-1 nested @condition arg bindings by name without argMapping. A @condition whose slot is an input object with scalar fields previously required an explicit argMapping to bind the condition method’s parameters to the nested fields, even when the parameter names already matched the nested field names exactly (the motivating searchVektingstallRange(table, fra, til) against SokVerdiRange { fra, til }); R355 drops that boilerplate. Core inference (ServiceCatalog.inferBindingsByType): a new name-keyed depth-1 branch runs on the parameters still unbound after the arity-unique and type-unique branches. For a parameter whose name matches exactly one direct field of a single unclaimed input-object slot, by name AND mapToJavaTypeName (so only canonical-scalar-typed leaves, never a named-input-object / enum / unclassified-scalar leaf the emitter cannot vouch for, the same null-is-no-match discipline the unambiguousReachablePath suggestion uses), it binds the parameter one level in via inferNestedFieldByName → PathExpr.step(head(slot), field, liftsList). The synthesised PathExpr is byte-identical to the one a hand-written argMapping: "p: slot.field" produces; liftsList is computed via ArgBindingMap.isListShaped (relaxed private → package-visible) so a list-shaped leaf is not hardcoded. Zero or >1 candidates leave the parameter unbound, so the existing per-parameter rejection / argMapping suggestion still fires; ranging over the unclaimed slot set keeps it a peer of its siblings and is what makes an ambiguous name fall through. Depth ≥ 2 stays explicit argMapping by design (deeper descent drags in recursive input types and path-dependent uniqueness; one hop is where the name still plainly names its source field). The disambiguator is the parameter name, orthogonal to R219’s count axis, so it lands as a distinct branch. Emit deviation (ConditionResolver.rewrapForNested): the spec’s "Files in play" said no emit change was expected (the PathExpr equals the explicit-argMapping one), but the execution test surfaced a latent gap shared by both forms: the input-field-@condition rewrap folded only the walk’s path to the input field and dropped each parameter’s own descent, so a multi-segment binding cast the whole wrapper Map to the leaf type (a defensive-cast-that-throws, against "classifier guarantees shape emitter assumptions"). A new nestedPath helper now appends the per-parameter path tail (segments after the head, the head naming the input field already at leafPath’s tail) to the `NestedInputField path; a bare-head (single-segment) binding returns leafPath unchanged, byte-identical for every pre-R355 binding, and the fix also completes the explicit-argMapping form for the same shape. The R214 ServiceCatalogTest case that deliberately yielded "to name-based matching" now asserts the concrete inferred binding (input.filmId) R355 produces instead of the suggestion it used to print, the intended R214 → R355 handoff (a strengthened assertion, and the type-unique yield it relies on stays implicitly exercised). Coverage: unit ServiceCatalogTest#inferNestedFieldByName_* (single scalar match, list liftsList=true, name-with-wrong-type → null, two-slot ambiguity → null); pipeline GraphitronSchemaBuilderTest (INFER_NESTED_CONDITION_ARG_BY_NAME asserting the inferred PathExpr chain, its *_EXPLICIT_ARGMAPPING_EQUIVALENT sibling asserting the identical values to pin equality, *_LIST_LIFTS_LIST pinning the computed liftsList=true, *_AMBIGUOUS_FALLS_THROUGH pinning the rejection, all on classifier-output PathExpr values not generated-body strings); execution GraphQLQueryTest#inputFieldCondition_nestedArgInferredByName_filtersSameAsExplicitArgMapping (a rental_rate [2.0, 3.0] round-trip selecting exactly the three 2.99 films, proving both bounds bind to the right nested field, asserted identical to the explicit-argMapping sibling). In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). Related: R249 (nested argMapping syntax, the explicit axis), R219 (unify arity-/type-unique under one JavaTypeKey count rule, the type-based inference this sits beside), R214 (the layered type-based inference this follows).

  • R329 (5b59e4a; spec 268e404, revised 21a0d36, Spec → Ready 8c414b6): Re-admit @service carrier payloads with a record-composite data field, landing R75 Phase 3 on the post-R276 reflection-driven binding model. An @service mutation whose method returns a list (or single) of a consumer-authored composite (a POJO bundling several jOOQ records, e.g. one FilmRecord plus a List<ActorRecord>) is now expressible as a two-level carrier: a payload whose non-@table object data field is a list of an intermediate result type whose @field-mapped @table children map onto the composite’s components. Previously this dangled (rejectDanglingTypeReferences) ; the result axis skipped the wrapper under the cardinality-match guard, and the carrier axis admitted only @table-typed data fields. Binding side: the cardinality decision is lifted into one builder-internal sealed ProducerBindLevel { BindsWrapper, BindsDataFieldElement, NoBind } read by both the single-level and two-level paths (the ArgumentRef-style classify-once/project-into-each-consumer pattern), replacing the old sdlIsList != reflectedIsMulti reject + carrier-admit pair that would desync into a dangle or double-bind; BindsDataFieldElement grounds the data field’s element type to the producer’s reflected return-element on the existing result axis, so no new ProducerBinding arm and no nullable-TableRef generalization of ServiceEmitted. Carrier recognition: carrierTableBinding becomes a sealed CarrierBinding { TableBacked, ClassBacked, NotACarrier } with one carrierVerdict projection shared by lookAheadVerdict, the producing-edge registration, and the nesting/orphan guards, so the verdict cannot drift; the composite carrier classifies as a class-backed ResultType naming the per-element composite, gated on the payload being @service-produced (BuildContext.isServiceProducedPayload, the single producer of that fact, shared with the errors WrapperArm selector) and not itself result-axis-bound. Emit side: a new ChildField.RecordCompositeField leaf, justified by a dimensional row (Record / bareFetch / listOrSingle(Record) / Plain(composite)) distinct from RecordField (Field target), RecordTableField (Table target), and ServiceRecordField (ServiceCall operation); a source-passthrough projection carrying its own SourceEnvelope (DIRECT / OUTCOME_SUCCESS) rather than recomputing it at emit, reusing the existing Outcome WrapperArm for errors with computeMutationServiceRecordReturnType re-levelled to the reflected method return. Validator mirror: the three near-misses surface through existing recognizers (mismatched producer → RecordBindingMultiProducer; a @field child neither @table-backed nor a resolvable composite accessor → accessor-mismatch; the re-levelled cardinality mismatch → checkServiceReturnMatchesPayload), not new predicates kept complementary by hope. Coverage: a ClassifiedCorpus entry pinning the payload classification + data-field verdict, a @ProjectionFor(RecordCompositeField) LSP-projection assertion, RecordCompositeField in IMPLEMENTED_LEAVES (GeneratorCoverageTest stays exhaustive), pipeline-tier positive coverage (ServiceRecordCompositeCarrierPipelineTest: list + single arrival, DIRECT/OUTCOME_SUCCESS envelope, errors WrapperArm, @table children as RecordTableField), validator-tier coverage of the three near-misses (ServiceRecordCompositeCarrierValidationTest), and an execution-tier round-trip in graphitron-sakila-example (GraphQLQueryTest: List<composite> projection + the error arm rendering data: null) backed by the cross-module compile against real jOOQ records; no generated-body string assertions. Surfaced by the utdanningsregisteret Graphitron 10 upgrade (opprettUtdanningsspesifikasjonOgUtdanningsmulighet). The batch-keyed Map<Key, List<composite>> shape stays out of scope for root carriers: a root @service returns the composite list directly, while peelReturnElement/isMultiCardinalityReturn already peel Map for the orthogonal child-batching path and a Map return at a root carrier rejects loudly via checkServiceReturnMatchesPayload (settled, not half-wired). In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R354 (46522ba; spec 64913b8, Spec → Ready 8e46e7f): Self-FK @nodeId on a Graphitron-owned single-row UPDATE routes all-SET with a cross-partition value-agreement check. The UPDATE sibling of R328 (which shipped the self-FK @nodeId @reference on the INSERT/read sides and the email/mailbox fixture). On @mutation(typeName: UPDATE) a self-FK whose child columns straddle the row’s identity key (email_in_reply_to_fk’s `(mailbox_id, in_reply_to_no), where mailbox_id is a PK member) previously tripped UpdateRowsError.MixedCarrierKeyMembership: the straddle check partitions at input-field granularity, so a self-FK overlapping the PK had no expressible UPDATE form. Design: a self-FK reference is a write of "who this row points at" (its parent), never the row’s own identity, so it routes its lifted columns wholly to SET regardless of key membership; the shared key column then appears in both the WHERE (from the identity field) and the SET (from the self-FK), ordinary SQL whose two decoded values the FK constraint forces equal. Marker: a selfReference boolean on InputField.ColumnReferenceField / CompositeColumnReferenceField, set once at the NodeIdLeafResolver discrimination site (where T.table() equals the containing table) and threaded through BuildContext; every other construction site passes false (the fact-lives-in-the-model lift, the walker reads carrier.selfReference() rather than re-deriving self-ness). Three UpdateRowsWalker sites fork on it: Stage 6 routes all-SET, Stage 4-5 computes key coverage over the non-self-FK columns only (a PK column reachable only via the self-FK correctly fails NoUniqueKeyCoverage, a self-FK cannot pin the row it lives on), and Stage 2b defers a self-FK on a bulk (list-input) UPDATE to R342 with a clear UnsupportedInputFieldShape reject rather than a silently-wrong FROM (VALUES …) derived table. The narrowed MixedCarrierKeyMembership still rejects a genuine cross-table FK straddle (a cross-table FK’s lifted column can legitimately be the row’s own identity, so it keeps partitioning by membership; fkTargetNodeIdRef_arity1_update_admitted pins that the all-SET rule did not leak). Emit: TypeFetcherGenerator.emitKeySetAgreementPreamble adds a cross-partition (WHERE∩SET) agreement preamble in the single-row UPDATE arm, decoding each side into a presence-guarded preamble-local and passing both through R322’s requireColumnAgreement (reused unchanged) before the DML; the throw names both contributing input fields. A second WHERE predicate was rejected as a silent drop wearing a no-match costume. This is the WHERE↔SET boundary R322’s four same-clause agreement sites never crossed, landed as a deliberate fifth instantiation of the gather-and-compare scaffold (the emitAgreementDecodeLocal / appendAgreementValue helpers are the seam, the carrier-agnostic writer-abstraction lift stays R342's). Coverage: UpdateRowsWalkerTest (all-SET routing, coverage-via-self-FK → NoUniqueKeyCoverage, bulk reject, cross-FK straddle still MixedCarrierKeyMembership), MutationDmlNodeIdClassificationTest (updateEmailReply classification + the marker on both carriers), graphitron-sakila-example compilation of UpdateEmailReplyInput against real jOOQ at Java 17, and execution-tier SelfFkNodeIdUpdateExecutionTest (agree repoints in_reply_to_no with the mailbox_id SET a no-op / disagree throws and rolls back, no silent row-move / omitted nullable updates subject only, no agreement check); no generated-body string assertions. docs/typed-rejection.adoc narrowed and RejectionSeverityCoverageTest annotated to reflect the self-FK no longer reaching the straddle arm. In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R349 (1db8756): Decouple service-half LSP goto-definition positions from the generator build and type the resolution outcome. Goto-definition on a @service / @condition / @externalField class or method reference silently no-opped in a live graphitron:dev session when the reference’s source root was scanned for bytecode (so completion worked) but not walked for positions: CatalogBuilder.enrichExternalReferences left the CompletionData.SourceLocation at the UNKNOWN sentinel (uri="", line=0), which Definitions collapsed to "no jump" through one uri().isEmpty() test, making the recoverable not-yet-indexed case indistinguishable from the two correct no-ops (binary-only source genuinely absent; overload-ambiguous). Change 1, typed outcome: a new sealed DefinitionTarget { Located, SourceAbsent, Ambiguous } (in graphitron-lsp/.../definition/) replaces the sentinel; Definitions resolves the service half through two pure FQN-join helpers (classTarget / methodTarget) and switches on the typed outcome exhaustively in one place (Definitions.resolve), where SourceAbsent logs a recoverable signal and Ambiguous is a deliberate silent no-jump. SourceWalker.Index now exposes the ambiguousMethods set the merge already computed and discarded, so Ambiguous is distinguishable from SourceAbsent. Change 2, source-cadence index: CompletionData.ExternalReference / Method drop their definition field; the LSP owns a volatile SourceWalker.Index on Workspace (sourceIndex() / setSourceIndex), seeded by DevMojo at startup and refreshed by a third watcher (sibling to the schema and classpath watchers) on .java over the compile source roots, so a declaration that moves in a hand-edited source is jumpable without waiting for a .class rebuild. Definitions joins ref-from-catalog with position-from-source-index at request time on the FQN both carry. CatalogBuilder keeps lifting Javadoc into description on the build cadence (hover untouched). Scope bounded to the service half (the reported bug); the jOOQ half, hover-description cadence, and the static SourceWalker.CACHE are deliberate transitional states, each documented and deferred to the follow-up R352 (complete-lsp-position-decoupling). Coverage: DefinitionsTest pins each DefinitionTarget arm reachable (Located / SourceAbsent / Ambiguous) plus the end-to-end class/method jumps; SourceWalkerTest pins the ambiguousMethods exposure; CatalogBuilderSourceTest asserts the build-cadence Javadoc lift now that positions are LSP-tier; CatalogRefreshTest pins a .java edit refreshing the source index without a catalog rebuild; no code-string assertions on generated method bodies. Predecessors R90 (the source walk) and R351 (the compileSourceRoots/classpathRoots parity stopgap). In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db).

  • R350 (8c88f5d): Workspace-wide LSP goto-definition for GraphQL type references. Goto-definition on an intra-schema type reference (the Film in films: [Film!]!, an implements interface, or a union member) previously resolved only when the file declaring the target type was in an open buffer; in a real multi-file schema the declaration is frequently not open, so the jump silently no-opped ([]) even though the TypeDefinitionRegistry already holds every type’s source position and ships to the LSP through the build snapshot. Producer: LspSchemaSnapshot.Built gains a per-type declaration-location map (typeDefinitionLocations(), keyed by SDL type name) plus a typeDefinitionLocation(name) lookup, threaded through both leaf records' canonical constructors and Workspace.demoteSnapshot; a new 5-arg convenience constructor on each of Current/Previous defaults the map empty so existing fixtures compile untouched. CatalogBuilder.buildSnapshot populates it from registry.types() + registry.scalars(), reducing graphql-java’s 1-based SourceLocation to the 0-based coordinates every goto-definition consumer reads (mirroring SourceWalker’s `-1); null-source built-in scalars and the bundled-directive source are dropped rather than emitted as dead file:// URIs, the bundled source-name exposed as RewriteSchemaLoader.DIRECTIVES_SOURCE_NAME. Consumer: IntraSchemaDefinitions.compute takes the snapshot as an explicit parameter (test seam matching the Definitions.compute sibling convention, since the provider’s only Workspace-snapshot install path demands full BuildArtifacts); the open-buffer tree-sitter scan stays first and authoritative, falling back to built.typeDefinitionLocation(typeName) on miss, and GraphitronTextDocumentService passes workspace.snapshot() at the call site. Coverage: IntraSchemaDefinitionTest (the three acceptance arms: snapshot fallback when the declaring file is not open, open-buffer precedence over a deliberately stale snapshot entry, neither-source no-op) and CatalogBuilderSnapshotTest (a producer-side arm over the real RewriteSchemaLoader.load parse path asserting user types/scalars land at 0-based positions while built-ins and bundled-directive inputs/enums are dropped); no code-string assertions on generated method bodies. Out of scope: the pre-existing 1-based sourceLocation(GraphQLScalarType) completion-feed helper (a separate code path, not this fallback). In Review → Done approved by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules incl. graphitron-lsp).

  • R328 (27d2359): Self-FK @nodeId reference on Graphitron-owned DML mutation inputs. A same-table @nodeId carrying an explicit @reference(path: [{key: ...}]) now means "follow this self-FK and write its child columns" instead of "use my own identity", read/write symmetric and the sibling of R315 (cross-table FK-reference @nodeId) and R322 (the shared-column dedup + agreement it rides on). D1 gates the NodeIdLeafResolver.resolve same-table own-PK short-circuit on @reference being absent; with @reference present the leaf falls through to resolveFkJoinPath, which orients the self-FK with selfRefFkOnSource=true and yields a Resolved.FkTarget.DirectFk whose liftedSourceColumns are the self-FK’s child columns on the row’s own table, the same data shape a cross-table FK carries, so no new sealed variant. The single shared gate lives in resolve(), so the shape is admitted on the read side too (a same-table @nodeId @reference query arg / filter resolves to a DirectFk self-FK filter, WHERE child_cols IN (decoded keys), no self-join, and the @asConnection same-table advisory correctly stops firing). D2 lifts the R315 InputBeanResolver.buildRecordKeyDecode same-table "self-reference out of scope" reject, routing the @service jOOQ-record case through the same BuildContext.resolveRecordFkTargetColumns the cross-table branch uses (oriented selfRefFkOnSource=true), landing the decode on the self-FK child columns, never the record’s own PK. D3 adds no emitter code: R322’s per-column structural dedup + requireColumnAgreement carry the shared-column overlap (the natural CAMPUS/email case where the self-FK’s first child column coincides with a cross-table FK’s), proven end to end. D4 pins cross-path consistency by test rather than a new abstraction: the same-table && !@reference ⇒ self-FK predicate plus the node-key reconciliation are duplicated across the classifier (permutationToKeyColumns) and record-population (resolveRecordFkTargetColumns) paths, so anti-drift tests assert both land identical child columns on the identity permutation (email fixture) and off it (the existing reordered_fk_child fixture forcing a non-identity permutation through both reconciliations). Coverage: resolver-tier NodeIdLeafResolverTest (DirectFk landing + without-@reference identity contrast + two anti-drift cases), MutationDmlNodeIdClassificationTest (INSERT admits the CompositeColumnReferenceField over the self-FK child columns, surfacing the shared mailbox_id), JooqRecordServiceParamPipelineTest (@service classifier, replacing the dropped reject test), SelfFkNodeIdReferenceReadSidePipelineTest (read-side DirectFk filter + no-@asConnection-advisory), and execution-tier SelfFkNodeIdInsertExecutionTest (agree inserts / disagree throws + inserts nothing / omitted nullable leaves the lone decode, on the email/mailbox fixture, jOOQ schema 1.8 → 1.9); no generated-body string assertions. Out of scope: the bulk UPDATE SET decode-overlap dedup, the one surface where a self-FK shared-column overlap still fails loud (a duplicate derived-table column), owned by R342 (Backlog); R328 only makes that gap reachable via a natural self-FK shape rather than a contrived one. In Review → Done reviewed by a session distinct from the implementer (27d2359). Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R322 (42ac9b2 @service jOOQ-record path D3+D4; 0535abb @mutation INSERT path D1/D2/D5; f739fd5 single-row UPDATE SET D3 + UPDATE-path D2; 0a9c8df In Review markup + R342 filing): Runtime value-agreement check for multiple @nodeId decodes onto shared columns. When more than one writer (two @nodeId decodes, a @nodeId decode plus a plain @field, or two composite FKs) lands on a single backing column, agreeing values are harmless but a disagreement would silently overwrite a caller-supplied value, the "no silent drops" failure the generator exists to avoid; the disagreement is only observable at runtime (values arrive off the wire), so it cannot be a build-time reject. D3 adds the shared predicate NodeIdEncoder.requireColumnAgreement(label, DataType, a, b): it coerces both sides through the destination column’s jOOQ DataType (the same coercion the real write applies, riding the class-level @SuppressWarnings for DataType.convert, so format-variant wire values like "01" / 1.0 / BigInteger 1 collapse onto the decoded 1 and agree, while a genuinely varchar "01" vs "1" still disagrees) and throws GraphqlErrorException on disagreement; one home for the message and semantics so the paths cannot drift. D1 resolves, per backing column, the ordered list of contributing writers, consumed by D4 (the @service JooqRecordInstantiationEmitter prepare/agree/load emission, byte-identical when no column overlaps), D5 (the @mutation INSERT structural dedup in TypeFetcherGenerator.insertColumnPlan driving the column list plus a single coalesced typed Field<ColType> VALUES cell, turning the Postgres "column specified more than once" crash into one column + one agreement-checked cell), and the single-row UPDATE SET agreement preamble (emitSetAgreementPreamble before the Map.put`s). D2 moves the build-time-decidable half (two-or-more plain `@field`s on one column, a pure schema fact no runtime input could reconcile) to a validate-time reject on both paths: `MutationInputResolver.rejectPlainColumnCollision (INSERT) and UpdateRowsWalker via the new UpdateRowsError.PlainColumnCollision (UPDATE, single-row + bulk), the mutation mirror of the R336 @service reject. All checks are presence-guarded (an omitted nullable writer is not a writer and cannot conflict) and pairwise against the first present writer (equals transitive). Scope call: extended past the spec’s INSERT focus to the single-row UPDATE SET path, and carved the bulk UPDATE SET decode-overlap to R342 (Backlog) on the grounds that the bulk UPDATE … FROM (VALUES …) join fails loud (a duplicate derived column) rather than silently, so it is self-announcing; with the single-row SET agreement and the all-plain reject in place, no silent drop remains on any mutation write path. Coverage: execution-tier NodeIdValueAgreementExecutionTest (the agree / disagree / presence-guard matrix across all three paths plus the format-variant "01" case pinning the coerced comparison, on storage_bin / film_endorsement fixtures), pipeline-tier JooqRecordServiceParamPipelineTest (two identity decodes / plain-field-plus-decode admitted and deferred to runtime) and MutationDmlNodeIdClassificationTest (the two-plain-fields rejects on INSERT and UPDATE SET), and the RejectionSeverityCoverageTest audit for the new sealed variant; no generated-body string assertions. Documented residual (recorded in the R342 follow-up): the column→writers overlap analysis now exists in per-path instantiations (@service, INSERT, single-row UPDATE SET, the two validate-time rejects); the shared predicate has one home but the structural grouping is re-derived per carrier model / pipeline stage, the lift to one carrier-agnostic writer abstraction deferred to R342 when the bulk-SET path forces it. Spec → Ready and In Review → Done both reviewed by sessions distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R344 (21915b9 exception + loader + DevMojo arm + tests; 1bf9ca7 self-review tightenings + R345 follow-on; 63b033d In Review): Surface schema parse failures as clean dev-watch diagnostics, not infrastructure stack traces. In the graphitron:dev watch loop a syntactically invalid schema (the common mid-edit case) dumped a ~30-frame graphql-java + executor stack trace into the build log on every keystroke, because RewriteSchemaLoader.load wrapped graphql-java’s InvalidSyntaxException in a bare RuntimeException and DevMojo.runGeneratorPass logged that through the "infrastructure" arm getLog().error(..., e) (with the throwable). A new typed SchemaParseException (a sibling of ValidationFailedException, not a subtype, in no.sikt.graphitron.rewrite) is now thrown from the loader’s InvalidSyntaxException arm only; the IOException / missing-file arms stay bare RuntimeException (genuine infrastructure, keep their trace). Its getMessage() is the existing file-attributed one-liner ("Schema parse failed in <file> at line N column M: <brief>`"), so the three already-quiet catalog-refresh paths (regenerate` / rebuildCatalog / buildOutputQuietly, which catch RuntimeException and print getMessage()) keep printing attribution unchanged; it also carries a nullable SourceLocation + brief consumer-less for now, positioned to feed the deferred LSP-squiggle follow-on (R345). runGeneratorPass gains a catch (SchemaParseException) arm ordered before the generic infrastructure arm that logs the one-liner without the throwable and resets previousErrorKeys = null (a parse failure is not a validator verdict, so it must not feed WatchErrorFormatter’s delta tracker). `GraphQLRewriteGenerator is unchanged: the exception propagates as-is through loadAttributedRegistry() out of all three entry points with no translation step, so the one-shot validate / generate build still fails on a broken schema carrying the attributed message. Two reviewed deliberate deviations: runGeneratorPass relaxed private → package-private as a test seam, and the consumer-less brief field carried per spec for R345. Rejected fork (documented in the retired spec): routing through ValidationFailedException + a fabricated ValidationError/Rejection.InvalidSchema, which would regress the quiet paths to a count string, falsify ValidationFailedException’s javadoc invariant, and stamp a pre-classification failure as a validator verdict. Coverage: unit `RewriteSchemaLoaderTest (throws SchemaParseException with the offending file’s SourceLocation + attributed getMessage() pinned exactly to location + brief as the quiet-path regression guard; still bare RuntimeException for a missing file), SchemaParseExceptionPropagationTest (generate() propagates the same type, no translation), and DevMojoTest (malformed schema → parse arm logs no throwable; missing file → infrastructure arm logs the throwable, pinning catch-arm ordering); no generated-body string assertions. Spec → Ready and In Review → Done both reviewed by sessions distinct from the implementer. Out of scope: the LSP red squiggle for parse failures (deferred to R345), wire/attribution mechanics, any change to ValidationFailedException. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R300 (8249abb fixture + jOOQ-regen foundation; 0074bd4 model + @routine directive + JooqCatalog.resolveTableValuedFunction + RoutineDirectiveResolver + emitter; 076cc56 execution-tier proof + routine.adoc docs + fixture gates; b0533b1 spec markup): First-class jOOQ routine support, day-one table-valued read slice. A new @routine(name:, argMapping:) directive backs a root Query field with a jOOQ-generated table-valued function (PostgreSQL RETURNS TABLE / SETOF). jOOQ models such a function as a first-class catalog Table<R>, so the slice rides the existing @table-bound return-type and selection-narrowing ($fields) machinery unchanged; only the FROM source differs, becoming a call to the schema’s global Routines convenience method with the routine’s IN parameters bound from GraphQL arguments. The provenance is carried by a new RoutineRef (the catalog-handle twin of @tableMethod’s `MethodRef) on a new QueryField.QueryRoutineTableField leaf, added to TypeFetcherGenerator.IMPLEMENTED_LEAVES so the four-way dispatch partition stays exhaustive; operation() is Fetch and target() projects a bare TargetShape.Table, following the QueryTableMethodTableField precedent. The deferred scalar-read and procedure-write forks reject at validate time via JooqCatalog.resolveTableValuedFunction (they do not resolve as table-valued functions), satisfying "validator mirrors classifier" without a STUBBED_VARIANTS entry, since no leaf is minted for them. Three reviewed deviations from the spec-as-reviewed: emission rides the Routines convenience method rather than <ROUTINE>.call(...) (same SQL); catalog-based discovery with IN-param names depending on -parameters compilation; classification reuses FieldClassification.QueryTableMethod (a dedicated QueryRoutine is a follow-up). Coverage: ClassifiedCorpus routine-table-valued-read fixture (Query / Fetch / List(Table)), a @ProjectionFor(QueryRoutineTableField) test in GraphitronSchemaBuilderTest, and RoutineFieldExecutionTest running the driving function (tilganger_for_feidebruker_med_fs_fiktivt_fnr) end-to-end against PostgreSQL including a selection-narrowing case; no generated-body string assertions. Spec → Ready and In Review → Done both reviewed by sessions distinct from the implementer. Deferred follow-ups (named in the retired spec): the procedure-write Operation write arm, scalar-function and record-returning reads, child-positioned @routine, heterogeneous binding sources, and the translation of legacy’s 26 procedureCall* rejection fixtures (the follow-up should also add pipeline coverage for day-one’s own resolver rejection arms, not only the legacy forks). Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db).

  • R336 (688a43c D1-D4 code + tests + CustomerRecordService fixture; doc-fix rework 13695d0): Flatten nested input-object fields in jOOQ-record @service params. A @service param typed as a generated jOOQ TableRecord can now group its columns under nested directiveless input objects that flatten onto the one backing table on the column axis, the analogue of the @table-input nesting the filter axis already supports. D1: CallSiteExtraction.ColumnBinding / RecordKeyDecode carry an ordered, non-empty List<String> access path (was a single sdlFieldName) with a leaf() accessor; the last element is the Map key, earlier elements the enclosing nested-input field names, and a top-level binding is a single-element path byte-identical to before (adopting the NestedInputField representation R186 settled). D2: InputBeanResolver.buildJooqRecord recurses into nested grouping inputs via a new collectJooqBindings, threading the existing ClassifyContext SDL-type-name expanding set for cycle detection, parallel to the member-axis buildInputBean walk rather than routing through classifyInputField (a different carrier family on a different axis). D3: typed Rejection`s through `JooqBuilt.Fail for cycle, list-valued nesting (a single record has one value per column), nested @table (a second DML target, cites R122), and plain-column collision across nesting (decode-vs-decode / decode-vs-column overlaps stay with R322’s value-agreement deferral). D4: JooqRecordInstantiationEmitter wraps each multi-element binding in a null-safe parent-Map descent (the instanceof Map<?,?> chain idiom from ArgCallEmitter generalised to statement form) with collision-free camelJoin-derived locals; an absent / null / non-Map group skips the columns under it, and a non-null identity inside an absent nullable group is skipped rather than thrown (skip-not-throw, since its R195 throw lives in the never-entered block). At depth 1 no wrapping block is emitted and the output is byte-identical to the pre-R336 form. graphql-java constraint surfaced in the execution tier: its coercion drops an explicit-null field from a nested input-object value (both literal and variable wire paths) while retaining it on the top-level argument map, so a present-null nested leaf is indistinguishable from omitted and leaves the column untouched; the top-level present-null → NULL three-way narrows to a nested two-way, with no emitted-code change. Coverage: pipeline JooqRecordServiceParamPipelineTest (8 new cases: flatten with two-element paths, mixed top-level + nested, nested @nodeId decode, depth-2, and the four D3 rejections by message substring) and 6 GraphQLQueryTest execution cases (lands-on-column + omitted sibling, present-null collapse asserted through the variable path, null group, skip-not-throw on an omitted nullable identity group, empty-input, malformed-id-in-present throws), plus the compile tier type-checking createCustomerRecord against the real jOOQ CustomerRecord; no generated-body string assertions. New CustomerRecordService fixture (CustomerRecord avoids the createFilmRecord per-record-class dedup clash on QueryFetchers). Spec → Ready and In Review → Done both reviewed by sessions distinct from the implementer. Out of scope: multi-table nesting, reclassifying the nested grouping type (the PojoInput(null) model/LSP-honesty wart is R337), any change to the @table-input nesting path. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R340 (006999c): LSP goto-definition for intra-schema type references. The cursor on a GraphQL type reference (a field/arg/input-field type, an implements interface, or a union member) now jumps to that type’s canonical type Foo { ... } declaration in whichever open workspace file declares it; previously goto-definition resolved only directive arguments (@table / @field / @reference) into the jOOQ-generated Java tree. Adds IntraSchemaDefinitions (a provider in the definition package parallel to hover/DeclarationHovers beside Hovers): it keys on the cursor sitting on a named_type reference name outside any directive, skips TypeNames.BUILTIN_SCALARS, walks open files via the per-URI lock-guarded Workspace.get fast-skipping on the immutable declaredTypes(), and resolves through the new DeclarationKind.findDefinition helper (returns the name node of the canonical non-extension declaration, so navigation lands on type Foo, not extend type Foo); the returned Location carries the real declaration-name byte range via Positions.toLspPosition, not the jOOQ path’s 0:0 placeholder. Wired after Definitions.compute with .or() in GraphitronTextDocumentService.definition(); the two paths key off disjoint syntax (a named_type never sits inside a directive argument) so they never contend. Coverage: IntraSchemaDefinitionTest (9 cases through a real Workspace: same-file, cross-file, implements interface, union member, input field, built-in scalar empty, unknown type empty, cursor-on-declaration empty, definition-wins-over-extension; assertions on the returned Location URI + Range, not walk internals). Out of scope: extend-block navigation, find-references, the JavaParser-gated jOOQ per-line refinement (R90). Filed and implemented as R335; renumbered to R340 on landing after a parallel session allocated R335 to a different item (the input-surface classify-and-emit walk fold) that reached trunk first. Spec → Ready reviewed and In Review → Done approved by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules incl. graphitron-lsp).

  • R316 (slices 1-3b landed pre-rebase in the squashed root; slice 4a 9f643a7 + 4b 09f3868; slice 5 e522fb4 + prose sweep c900455 + code-generation-triggers.adoc 3e3f607 + changelog forward-notes 903e84d; In Review transition 70ce94e): Pivot the field-dimensional model from carrier × intent × mapping to (source, operation, target). A field is an edge: it arrives into a source (a wrapper around a SourceShape, the arm being the arrival cardinality and the emit-strategy dispatch: Root | OnlyChild | Child), performs an operation (a sealed interface with payload-carrying record arms replacing the flat Intent enum: Fetch / Paginate / Lookup / ServiceCall / Count / Facet / Nest / NodeResolve / EntityResolve / the writes), and projects a target (a Single | List wrapper around a TargetShape, output cardinality read off the GraphQL return type). SourceShape ⊆ TargetShape; cardinality lives only as a wrapper bound to an endpoint, never a free enum (the wrapper-algebra invariant). Built populated, not abstract: ServiceCall collapses the former QueryService / MutationService verb pair (read/write is the Source.Root.Query / Source.Root.Mutation legality gate now), holding the two un-unified call carriers in a transitional Call holder pinned to R314; the fused Mapping.TableConnection decomposes into Target.Single(Connection) (shape) + Operation.Paginate (windowed-read verb); declared-gap arms (Count / Facet / EntityResolve / UpdateMatching / DeleteMatching, plus the Interface / Union participant payloads) are modeled-but-unpopulated with documented known-gap entries. Slices: 1 rewrote the R222 model (docs); 2-3 introduced source() / operation() / target() additively (bridges deriving the retired axes so the corpus classified unchanged); 4a re-derived OutputField.requiresReFetch() + the dispatchPerformsReFetch validator mirror over the new axes (the bare TargetShape.Table × holds-records guard, behaviour-preserving against mapping() != Mapping.Table); 4b migrated the @classified R281 corpus onto source: / operation: / target: + sourceShape: / targetShape:, recut DimensionTuple to compare at the altitude the directive can express (Source by structural equality, Operation by arm type token, Target by (wrapper, outer-shape) token pair), and deleted the bridges and the four retired types (Carrier / Intent / Mapping / SourceCardinality); 5 landed the thoroughness gate. Tests: WrapperAlgebraTest (the sourceWrapperIsTheFoldOfAncestorTargetWrappers invariant, target half mirrored against the parsed SDL output wrapper, source half pinned at the conservative Child strength R305 builds, with the connection decomposition and scalar-projection-leaf exemptions guarded against rot); RetiredDimensionTypesAreGoneTest (type-resurrection backstop: deleted files, no model-package imports, distinctive names Intent / SourceCardinality absent as whole words, carve-outs SourceShape / LookupMapping / MappingEntry retained); ClassifiedDslTest.everyDimensionValueIsExercised (the disjoint-exhaustive coverage partition over the source / Operation seals with the SDL-vs-Java name mirror); ReFetchDerivationTest migrated behaviourally onto the new axes; SourceShapeProjectionTest retained as the source-arm projection guard. No code-string assertions on generated method bodies. The recommended leafReconstructsFromCoordinate completeness pin is deferred (the old flat-enum corpus never pinned payloads either, so nothing regressed). The dispatchPerformsReFetch mirror survives R316 by design (retiring it is R314’s emit re-platforming). Downstream: the SourceKey decomposition becomes the first concrete consumer once this pivot lands. The R290 / R299 / R305 changelog entries carry forward-notes flagging their dimensional vocabulary as historical. In Review → Done review (claude/r316-review-mj53fc) swept three residual retired-vocabulary remnants slice 5 missed (two dangling {@link #intent()} / {@link #mapping()} method-links to deleted methods in OutputField / ChildField, and ReFetchDerivationTest’s class-prose) onto the new vocabulary. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules incl. `graphitron-lsp).

  • R315 (0bb7161 + nullable-same-table-identity execution rework 0d4acca): Bind FK-reference @nodeId onto jOOQ-record @service params. Generalizes R311’s same-table identity case to cross-table foreign-key references: a @service parameter typed as a generated jOOQ TableRecord can now be populated from an input whose @nodeId fields reference other node types (the status / history / junction-row shape), with each decoded key mapped through the catalog FK constraint to the FK’s child columns on the record. Ports legacy NodeIdReferenceHelpers.mapKeyColumnsThroughForeignKey into the rewrite’s model. Model (D1): CallSiteExtraction.JooqRecord.keyDecode (Optional) becomes keyDecodes (List, so a record may carry several @nodeId fields); RecordKeyDecode generalizes R311’s keyColumns to targetColumns (the resolved columns on this record the decoded values load into, identity or FK-child) and gains a nonNull flag. Deliberately no KeyProjection sub-axis: both arms load targetColumns identically, the identity-vs-FK distinction lives only in the resolver, and nothing downstream branches on it. FK resolution (D3): the FK-orientation-and-pairing core extracts out of BuildContext.synthesizeFkJoin into resolveFkSlots, so the join path and the new resolveRecordFkTargetColumns share one bug-fixed orientation site (parent columns from ForeignKey.getKeyFields(), not getKey().getFields(), which mis-pairs a composite FK whose referenced-column order differs from the parent PK order); target columns reconcile to node-key (decode) order by column identity, not positional zip (a reordered FK whose referenced order differs from the node key would otherwise mis-assign every value). FK deduced when exactly one connects the two tables, else named verbatim by @reference(path: [{key:}]) (only the first path element is consulted for record population). Convergence by rejection (D2): @table on the input classifies it as TableInputType ("Graphitron owns the DML"), which contradicts a jOOQ-record @service param ("the service owns the DML"); InputBeanResolver keeps R311’s JooqTableRecordInputType trigger and adds a narrower isTableRecord reject arm so a @table-present record param fails honestly ("drop @table…") instead of falling to the bean path’s misleading "has no fields matching." Null semantics (D4): JooqRecordInstantiationEmitter switches from a single fromArray batch to per-binding conditional loads keyed on raw.containsKey(...), applied uniformly to @field columns and each @nodeId decode: a non-null (ID!/!) binding always loads and throws on a null / wrong-type decode (R195); a nullable (ID) binding leaves an omitted column unwritten (changed=false, excluded from the service’s INSERT/UPDATE), sets a present-null to NULL via set(field, null) (reliable changed flag; fromArray null-skips), and decodes-and-loads a present value; coercion stays on the non-deprecated fromArray path. Two R311 behavior changes carried, folded in deliberately (D4): (1) R311’s same-table identity single fromArray batch becomes per-binding conditional loads; (2) a nullable (ID) same-table identity moves from always-throw-on-null to skip-when-omitted (a service-side upsert input: omitted → unset PK → the service-owned INSERT lets the DB assign it). The emitter’s "two disjoint fromArray groups" javadoc and the RecordKeyDecode "always throws … whether ID! or ID`" javadoc are both retired so neither becomes a false invariant. Rejections (build-time `UnclassifiedField): zero/multiple FK without @reference(key:); a node key column not covered by the chosen FK; an explicit @reference on a same-table @nodeId (a self-FK request, out of scope, preserving legacy’s loud forbiddance instead of silently writing the record’s own PK); (unchanged R311) @field→no column, cardinality parity, @nodeId without typeName. The R311 single-@nodeId gate is removed: multiple @nodeId is now legal (each resolves independently; overlapping-load-column value-agreement is a runtime concern deferred to R322, last-write-wins here, never hit by the motivating consumer whose references are disjoint-column). Coverage: pipeline JooqRecordServiceParamPipelineTest (21 cases: FK deduction, the renamed-FK target column endorsed_film≠film_id real pin, reordered composite-key decode-order reconciliation, explicit @reference(key:) disambiguation on the two-FK studierett, the @table reject, mixed identity + FK + plain @field, the full rejection set, the former twoNodeIdFields_reject flipped to a positive two-keyDecodes classification) + SynthesizeFkJoinReorderedKeysTest (slot orientation pinned for both consumers); execution (sakila) the FK-child INSERT readback proving the decoded id lands on the renamed endorsed_film child column, the nullable FK-reference / nullable plain-column omitted-vs-null-vs-set changed-flag contract, and (rework) the nullable same-table identity on AddressRecord (omitted → DB-assigned serial PK via the service-owned INSERT, set → decoded id on the PK), the only tier that observes changed=false exclusion; compile (graphitron-sakila-example) createFilmEndorsementRecord against real jOOQ at Java 17. Adds the public.film_endorsement renamed-FK fixture (FK child endorsed_film ≠ referenced film_id) and reuses idreffixture.studierett / nodeidfixture.reordered_fk_child / child_ref. User docs: FK-reference @nodeId on @service jOOQ-record params documented in nodeId.adoc. Generalizes R311; the @table-on-input deprecation / argMapping grouping stays R97, self-reference and pojo member-axis FK-@nodeId are separate items, overlap value-agreement is R322. Motivated by four utdanningsregisteret-graphql-spec status-mutation consumer shapes. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R325 (folded into R317, no implementation shipped): Classify in a single field-first visitor walk (retire the eager type pass). Filed 2026-06-17 as a from-first-principles restatement of R317’s goal, then folded into R317 the same day and discarded as a separate item. Its substance lives in R317’s "Read-free visitor invariant and the single walk" section: the read-free visitor invariant (the classifying visit may only register, never read the registry under construction), parent context down the SchemaTraverser var channel, reconciliation in the registry (the visit stays pure), the parent-independent-entry guard that dissolves the dedup worry, the explicit non-field edges (@node/@key seeds, interface<→object structural edges, per-usage input resolution), validations as named post-walk passes, the anti-narrative slicing rule (no remaining slice may be structure-only), and the falsifiable acceptance test (no type registered before its discovering field is visited). The one mechanism fork it raised, no global pre-pass fixed points, was settled by treating a precomputed fixed point passed as an explicit traverser argument as read-free-compatible: the NodeIndex stays precomputed and threaded in, reflection grounding becomes on-demand (no RecordBindingResolver.resolveAll() precondition). R317 stays backwards-compatible: it restructures type classification and keeps the leaf ChildField model untouched; the genuinely distinct idea this thread surfaced, classifying fields directly into the dimensional (source, operation, target) model so the field carries its own type/table binding, is a separate switch off the leaf model that lives with R316 (the (source, operation, target) pivot) and R314 (dimensional emit, where the leaves dissolve), not with the R317 driver. (Independently discarded from trunk’s side as 1ac9c75; the two discards reconciled here.) Number retired, not reused.

  • R324 (45bf3cb): Lift the single-cardinality multi-hop @splitQuery restriction. A single-cardinality @splitQuery child field (returns T, not [T!]) whose @reference(path:) had more than one hop was rejected at classification time (FieldBuilder.classifyObjectReturnChildField’s `FieldWrapper.Single && elements().size() != 1 Rejection.deferred), even though the supporting classification machinery (deriveSplitQuerySource keying off path.get(0), BuildContext.buildParentCorrelation pinning only firstHop) was already hop-count agnostic. The gap was entirely in SplitRowsMethodEmitter: of the three cardinality siblings only buildSingleMethod never grew the bridging-hop loop, projecting/FROMing off firstAlias with a single-hop-only (firstAlias, firstAlias) per-hop WHERE shortcut. The fix extracts two shared private helpers retiring the list/single/connection topology duplication that caused the drift, emitFromBridgeAndParentJoin (FROM-terminal + bridging-hop loop + OnConditionJoin parent JOIN + parentInput correlation) and buildWhereCondition (per-hop FK whereFilter`s + field-level filters); all three siblings route through both, and the connection WHERE loop’s unconditional `(JoinStep.FkJoin) cast becomes the shared instanceof-guarded form. buildSingleMethod now projects/FROMs off terminalAlias and bridges multi-hop, returning scatterSingleByIdx unchanged; single-hop paths collapse the bridging loop to a no-op, so the RecordTableField emitsSingleRecordPerKey() path that also routes through buildSingleMethod emits exactly as before (audited, no change required). Bridging hops are inner joins consistent with the siblings: a to-one chain resolves to null when any hop is absent; distinguishing intermediate-null from terminal-null (LEFT JOINs) stays out of scope. The classifier guard is removed; the motivating Customer.storeAddress shape (customer -> store -> address) now classifies as SplitTableField with a 2-hop joinPath. Coverage: pipeline GraphitronSchemaBuilderTest.SPLIT_TABLE_MULTI_HOP_SINGLE_CARDINALITY (the former _REJECTED enum, now a positive 2-hop FkJoin assertion keyed on store_id), execution GraphQLQueryTest.splitTableField_singleCardinality_multiHop_bridgesToTerminalAddressPerCustomer + _dedupesSharedKey_oneBatchRoundTrip (correct Address per customer matching the inline storeAddress navigation, and a single batched rows-method round-trip); the null-where-no-match semantic is structurally covered by ScatterSingleByIdxTest + the existing single-hop null-FK fixtures since Sakila’s customer.store_id / store.address_id are both NOT NULL. The now-false "single-cardinality multi-hop requires split" bullet is dropped from split-vs-inline.adoc (the shape works inline and via split). Emitter-only; no model-shape, directive, or wire-format change. Motivated by a sis-graphql-spec consumer shape. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db).

  • R23 (c38779e): Multi-parent NestingField sharing, TableField arm. Lifted the multi-parent shared-shape gate in GraphitronSchemaValidator.compareNestedFieldsShape for ChildField.TableField: a plain-object NestingField type may now be shared across multiple @table parents when its shared leaves include inline TableField`s, where before the catch-all rejected them with "not yet supported across multiple parents". The arm admits the pair without further shape comparison, the upstream class-equality gate (continue` on a getClass() mismatch) already guarantees both sides are TableField, and returnType() derives from the single SDL declaration on the shared nested type so it is identical by construction; per-parent joinPath / filters / orderBy / pagination are intentionally not compared because each parent’s $fields emits its own correlated DSL.multiset arm. No emitter or wiring change: TableField is a PROJECTED_LEAF whose reified read (FetcherEmitter.bind, wrapped in LightFetcher) pulls by field name from the source Record without consulting the outer parent table, so first-parent-wins nested-type registration has no runtime effect for this leaf. Coverage: pipeline GraphitronSchemaBuilderTest#multiParentSharedNesting_inlineTableFieldLeaf_classifiesAndValidatesPerParent (two @table parents sharing a nested type whose address leaf classifies as a TableField with a distinct per-parent FK joinPath; validator emits no error), execution GraphQLQueryTest#multiParentSharedNesting_inlineTableField_returnsAddressPerParent (shared OccupantLocation across Customer and Store, each resolving address per parent, the Customer side pinned to the same row as the direct Customer.address FK navigation), and the existing NestingFieldValidationTest non-TableField rejection stays green. Retired the stale #8 roadmap pointer in the catch-all comment; filed the BatchKey-leaves follow-up (and the open LookupTableField re-scoping question) as R323. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R321 (6601e39): One-shot mojos (graphitron:validate / generate) now render ValidationFailedException.errors() in the Maven failure output, at parity with DevMojo and the SchemaProblem branch. AbstractRewriteMojo.runGenerator previously special-cased SchemaProblem but let ValidationFailedException fall through to the generic catch (RuntimeException e) arm, which rethrew only the exception’s message (the bare N schema validation error(s) count) so a consumer build (opptak-subgraph) saw no file:line:col detail. The fix adds a sibling catch (ValidationFailedException e) arm ahead of the generic one, mirroring the SchemaProblem arm: it wraps the cause in a null-message intermediary (so Maven’s DefaultExceptionHandler does not append the bare count after the detail) and keeps the exception on the cause chain for -e / -X. Rendering is factored into a package-private validationFailureMessage(List<ValidationError>) that prepends a "GraphQL schema validation failed:" header (matching the SchemaProblemDiagnostic arm) to WatchErrorFormatter.format(errors, null), the same renderer the graphitron:dev loop uses (null previous-key set drops the dev-only delta line), so the one-shot and dev surfaces share one renderer and cannot drift. This covers errors raised at any build stage (validate(), the GraphitronSchemaBuilder.buildBundle federation-recipe rewrap, or TagLinkSynthesiser.apply). Coverage: AbstractRewriteMojoTest asserts the message carries per-error file:line:col detail (not just the count) and embeds the exact tree WatchErrorFormatter.format(errors, null) produces (structural dev-loop parity), at the same formatter-level tier as the sibling SchemaProblemDiagnosticTest. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db).

  • R318 (subsumed into R317, no implementation shipped): Validation registers diagnostics without reclassifying (immutable validate phase). Filed 2026-06-17 as a follow-on to R317’s single classification pass, then inlined into R317 the same day as its closing slice when R317 was rescoped from a byte-identical reorder to the full single-edge-driven-classify-pass + immutable-validate arc. No standalone work shipped; the immutable validate phase lands under R317. Number retired, not reused.

  • R279 (slice 3b inversion ee77a33, slice 4 507242f, slice 5 4a1a117, slice 6 prune 0e13c12 + verb-collapse 2437a84; In Review transition df6c1f7; slices 1/2/3a landed pre-rebase): Field-first reachability-driven classification driver. Replaces the eager type-pass / all-objects field-pass / four-post-pass sequence with a reachability-driven, field-first walk that classifies each type as a byproduct of the field edge that reaches it, then validates, then emits. R222 slice; supersedes R166 (the reachability prune is structural here, not a per-emitter skip-filter), and spins R166’s typed-non-empty-carrier sub-thread to R280. The walk seeds Query + Mutation + Subscription roots plus a @node/@key directive scan, and descends output edges (field→target, union→members native; interface→implementor and object/interface→interface via the custom SchemaTraverser child function, the load-bearing fan-out that keeps a directly-seeded federation implementor from pruning the Node interface its implements clause references). The accumulator owns reconciliation: TypeRegistry.register is the sole write verb (the Q2 verb-collapse), reconciling repeated registrations three ways (equal → idempotent, compatible → merge incl. the cross-carrier federation @tag union and shareable OR for synthesised Connection/Edge/PageInfo, incompatible → demote to UnclassifiedType); the former classify/enrich/synthesize/demote verbs dissolve into it, the trace Op derived from the reconciliation arm so per-call observability survives. The classifier is a pure producer reading only node SDL + reflection + downward TraverserContext context (never sideways/back), making verdicts order-independent; TypeBuilder.findReturnTablesForInput’s global back-scan dissolves into a local field-visit read. What shipped across the slices: order-independent `participantClassification (3a, replacing the sideways ctx.types.get read); the field-first driver inversion with compensating orphan sweeps (3b); DomainReturnType enforcement relocated from a reclassifying post-pass to a GraphitronSchemaValidator rule (collectDomainReturnTypeConflicts + validateUniformDomainReturnType, model change + validator rule in one commit, no enforcement gap); ConnectionPromoter folded into the walk as synthesiseForField with a single-producer rebuildAssembledForConnections (5); orphan prune made observable (an unreachable @table object is no longer classified) and the verb-collapse completed (6). The down-the-walk context admits ancestor-cardinality accumulation as a first-class rider so R308 can compute the source-cardinality ancestor-product without re-walking; R279 itself stays behaviour-preserving and does not compute it. Honest residual: the "true single-pass DFS fold" (approach A, inlining type classification into the field visit and deleting TypeBuilder.buildTypes) is deferred to R317 (now Ready); the driver is field-first and the walk is the sole classifier, but buildTypes still hosts the type-classify loop over the walk’s reachable set. Coverage: SchemaReachabilityTest (the reachable ⊆ classified safety invariant, hardened at slice 6 to "every classified output composite is reachable"), ConnectionAssembledDeltaPipelineTest (the assembled-schema delta, the projection differential’s blind spot), ProjectionSnapshotComparator (dev bisect aid only, never the gate), and the designated primary gate throughout: the GraphitronSchemaBuilderTest exhaustive truth table + sakila pipeline TypeSpec + the Java-17 graphitron-sakila-example compile + the PostgreSQL execution tier; assertions are on classified-model/assembled-schema structure, no generated-body code-string matches. FieldBuilder’s ~5660 lines of per-field logic untouched (the change is the driver, reachability, and registration); LSP `TypeClassification/FieldClassification projections confirmed unaffected. Forward edge R279 → R308; supersedes R166. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R311 (c0e8626): Bind a jOOQ TableRecord (singular Record or List<…>) directly as a @service input param. A top-level @service parameter whose Java type is a generated jOOQ TableRecord already classified as JooqTableRecordInputType on the type side but could not be bound at the call site: InputBeanResolver.enrich bean-ified it on the Java-member axis, so a @field naming a column matched nothing and the build rejected with a misleading "has no fields matching". enrich now reads the already-classified type (table and all) just after the shared input-object gates (loadable / Map / cardinality-parity) and binds on the column axis instead: each plain @field field resolves to a ColumnRef (a CallSiteExtraction.ColumnBinding), and a single @nodeId field decodes the record’s scalar key (R195’s wire mechanism projected onto the param record’s own identity, a RecordKeyDecode); a lifted record-type-mismatch gate rejects a foreign-table @nodeId. New model: the CallSiteExtraction.JooqRecord permit + the column-axis ColumnBinding / RecordKeyDecode records (siblings to the member-axis InputBean / FieldBinding, each carrying its own sdlFieldName Map key since neither rides a FieldBinding), with a compact-constructor at-least-one-binding floor; and ValueShape.JooqRecordInput, a path-carrying leaf that also carries its construction carrier (the Scalar-carries-leafTransform precedent) so the helper-queue collector registers from the ValueShape alone. A new JooqRecordInstantiationEmitter emits a deduped create<Record>(Map) / create<Record>List(Object) helper pair (record fromArray(…, Tables.<T>.<col>) for the columns, no deprecated DataType.convert, + NodeIdEncoder.decodeValues for the identity), reached identically from the root emitter (ServiceMethodCallEmitter via the new leaf) and the child-coordinate emitter (ArgCallEmitter real arm) ; the binding is coordinate-agnostic (enrich runs for child @service too, before the isRoot gate) and both cardinalities share one construction site (the plural maps the singular per element). The TypeFetcherGenerator dual walk feeds one record-class-keyed dedup queue so either coordinate emits the helper exactly once. The two sealed additions force-flag every exhaustive switch: real arms where reachable (valueShapeExpression / listExpression / ArgCallEmitter.buildArgExtraction / collectFromValueShape), defensive/throw arms where a JooqRecord(Input) is never an InputBean field leaf. The misleading bean message becomes honest, validate-time UnclassifiedField rejections (foreign-table @nodeId, two @nodeId, unresolvable column with a Levenshtein candidate hint, cardinality mismatch at the shared parity gate). Coverage: JooqRecordServiceParamPipelineTest (11 cases: singular, composite key, list → ListOf(JooqRecordInput), the regression pin for the original bug, the child coordinate, and the rejection set), four GraphQLQueryTest execution cases round-tripping the identity decode + column SET against PostgreSQL (singular, composite, list, wrong-type-throws), and the sakila-example compile tier type-checking the emitted helpers + the child ArgCallEmitter call against the real catalog; no generated-body string assertions. A call-site param-binding NOTE lands on the @service surface in code-generation-triggers.adoc (the classification was already documented). Out of scope: Set<TableRecord> (inherits the InputBean path’s imperfect Set handling), FK-reference @nodeId and @table-on-input (both R97). Motivated by the endreUtdanningsspesifikasjonsstatus(List<…Record>) consumer shape. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R310 (da56aa8): Name the forbidden directive on an otherwise-valid DML payload carrier’s data field. A @mutation(typeName: INSERT/UPDATE/DELETE) field whose payload’s single data field carried a DML-forbidden directive (e.g. @splitQuery) was rejected with the misdirected generic "is not yet supported; use ID or a @table type", pointing at the (fine) return type rather than the one-token edit on the data field that actually disqualified it. A new BuildContext.diagnoseForbiddenCarrierDirective would-admit-but-for-the-directive probe re-runs the structural DML scan under a private ForbiddenDirectivePolicy.IGNORE gate, leaving the public scan contract (scanStructuralDmlPayload / scanStructuralServiceCarrierPayload and every speculative caller) byte-for-byte unchanged; when the payload would admit as a carrier but for the forbidden directive, the single ScalarReturnType arm of MutationInputResolver.validateReturnType (where all three DML kinds converge) surfaces a targeted message naming the data field and the @-prefixed directive, with the @service-carrier @splitQuery asymmetry note (R275, the warnIfSplitQueryOnRecordParent advisory) appended conditionally. The generic message stays the fall-through for genuinely unsupported scalar returns. The rejection keeps the uniform prose Rejection.structural shape of its sibling validateReturnType arms; a typed ReturnTypeError sub-seal lift is deferred. Coverage: four MutationDmlCase pipeline cases (UPDATE + INSERT @splitQuery through two distinct routing paths pinning the "one arm covers all DML kinds" invariant, a non-@splitQuery @condition, and a negative-control would-not-admit two-data-channel payload confirming the probe does not over-fire), asserting load-bearing tokens on UnclassifiedField.reason() with no generated-body string assertions. Discovered during the utdanningsregisteret Graphitron 10 migration; sibling of R213 (message vs location). Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R305 (slice 1 b3f0f68, slice 2 355f9b5, slice 3 collapse 7e672b9 + re-fetch derivation d829d42, source-shape mirror 18d3aee; In Review transitions c6847c6 + 18d3aee): Forward note (R316, 2026-06-19): the carrier / intent / mapping / source-cardinality model this entry describes was pivoted to (source, operation, target); the Carrier / Intent / Mapping / SourceCardinality types named below no longer exist. Read the dimensional vocabulary here as historical. Expand the carrier dimension with source-shape and cardinality; separate re-fetch from intent and collapse SingleRecordTableField into RecordTableField. R222 Stage 3 follow-on to R290. The carrier dimension gains a Source-arm source-shape (Table | Record, the input-side mirror of mapping’s `Table:Column :: Record:Field) and source cardinality (One | Many); Carrier is now sealed (Query / Mutation payload-less, Source(SourceShape, SourceCardinality)). The deeper correction (settled 2026-06-15): intent and re-fetch are orthogonal. Intent classifies the target and how arguments are interpreted (Fetch, Lookup, the writes, QueryService); re-fetch is the derived Record-to-Table crossing. OutputField.requiresReFetch() derives Table mapping × holds-records (Source{Record} received or a Service/DML intent produced), not from intent alone, catching the whole family (former-SRTF, RTF, RLTF, RTMF, STF); GraphitronSchemaValidator.dispatchPerformsReFetch mirrors it. SingleRecordTableField is deleted: the two FieldBuilder carrier sites (R178 DML, R275 @service) now build RecordTableField via buildPayloadCarrierRecordTableField (a single JoinStep.LiftedHop over the target PK folding source=target onto one ColumnRef, plus a new SourceKey.Reader.ProducedRecordRead); intent stays Fetch. The runtime-call insight that settled it: SRTF and RTF are the same shape, the carrier field is called once with the producer’s full held output and feeds the same Split-rows VALUES(idx,pk) JOIN … ORDER BY idx scatter; only the key reader differs. Source cardinality is conservatively hard-coded Many (the absorbing element, always-correct as a one-element batch); the inline One-skip optimisation is kept as dead code until R279 computes the true ancestor-product cardinality. OrderingOwnedByProducer is deleted, its validateListRequiresOrdering exemption replaced by a plain requiresReFetch exemption (also fixing a latent bug: a PK-less idx-ordered re-fetch is now admitted rather than wrongly rejected). The 352-line FetcherEmitter SRTF path is removed with the LocalContext null-source guard preserved; the LSP FieldClassification.SingleRecordTable projection collapsed into RecordTableTarget. One honestly-documented divergence: dispatch routes by leaf identity (every RecordTableField batches) rather than literally reading the Many slot at the fork; net effect matches the spec, R314 filed for the follow-up. Coverage: ReFetchDerivationTest (true across the Record-source family + mirror agreement), SourceShapeProjectionTest (walks every corpus-demonstrated ChildField and cross-checks sourceShape() against the parent type’s independently-classified backing ; TableBackedType → Table, else Record ; exercising both arms, with a reflective sealed-leaf guard over ChildField whose uncovered leaves must carry a documented NOT_CORPUS_COVERED entry; the validator-mirrors-classifier analogue of dispatchPerformsReFetch), the @classified corpus grown with sourceShape / sourceCardinality args and the former-SRTF row retargeted to RecordTableField / Source{Record, Many} / intent Fetch, SingleRecordPayloadPipelineTest instanceof retargets, GeneratorCoverageTest.everyGraphitronFieldLeafHasAKnownDispatchStatus exhaustive/disjoint after the leaf deletion, and the load-bearing execution tier (SingleRecordPayloadDmlTest / SingleRecordTableFieldServiceProducerExecutionTest) preserving R141 / R158 / R275 payload-carrier behaviour (single + bulk, DIRECT + OUTCOME_SUCCESS, fjernSakTagg) end-to-end against PostgreSQL through the batched Split-rows path; no method-body code-string assertions. This is a deliberate leaf change, not byte-invariance: the former-SRTF coordinate’s emitted SQL becomes the batched idx-ordered VALUES-join scatter and its re-fetch verdict flips to true, while the runtime result (same rows, same source order) is preserved. Out of scope: the @service carrier arriving as a list (R308), target-cardinality-many on the One path, and the broader SourceKey.Cardinality wrapper().isList() disentangling (rides R222). Full reactor green (mvn install -Plocal-db, 11 modules incl. graphitron-lsp).

  • R309 (82f2ba3): Descriptions for query-as-view projections. A # ... line comment authored above a selected coordinate in a corpus doc-example projection query now renders as that coordinate’s SDL description in QueryViewRenderer output: above a field it describes the field, above ... on T or a top-level fragment f on T it describes type T; multiple comment lines join into a block-string """...""" description, and a comment-free projection renders unchanged (the existing seven QueryViewRendererTest cases stay byte-equal). Touched gains type/field description side tables, Walk records them through the single descriptionOf(Node) source seam (where native executable getDescription() reads on FragmentDefinition / VariableDefinition fold in once graphql-java is bumped past the pinned 25.0), and prune / keptFields / stripInternalDirectives stamp them onto the rebuilt DescribedNode`s via a shared `applyDescription helper. Field prose stays comment-sourced because Field is not a DescribedNode in any graphql-java version, making comments the durable carrier rather than a stopgap. This is test-and-docs tooling under src/test, not the production generator path: the catalog corpus example’s query() gains comments so its rendered block on code-generation-triggers.adoc carries field descriptions, and ClassifiedDocTest guards that block verbatim. Coverage: five new QueryViewRendererTest pipeline cases (field description, inline-fragment type, top-level-fragment type, multi-line block string, comment-free no-regression pin), all asserting on rendered SDL rather than generated method bodies. Out of scope: the graphql-java version bump and native-description source read (the descriptionOf extension), descriptions on production generator output, and operation-level (query { ... }) descriptions. Full reactor green.

  • R200 (e7be7f4): Honor @field(name:) in InputBeanResolver for @service input-bean/record member binding. The resolver bound consumer-bean/record members by raw SDL-field name, reading zero directives: a Java member name diverging from the SDL field name lost the binding (JavaBeans rejected with "no fields matching"; records silently emitted an under-arity canonical-constructor call). bindingKey(f) now reads @field(name:) per SDL field as the Java-member binding key (the house argString(f, DIR_FIELD, ARG_NAME).orElse(f.getName()) idiom, the input-side mirror of R191’s output accessor axis), and buildInputBeanBody is restructured: the record-vs-JavaBean target is computed once and dispatched via switch (target) into bindRecord / bindJavaBean (the mid-loop isRecord() re-tests collapse; recordOrder / sdlOrder deleted); a single collision-checked sdlByBindingKey index shared by both arms rejects two SDL fields resolving to one member (ambiguity) and a present-but-blank @field(name: ""); the record arm enforces a total bijection in bindRecord (direction A: every component must bind, was a silent under-arity drop; direction B: every SDL field must be consumed, was a silent data drop), retiring the dead "has no component named" branch by construction, while the JavaBean arm keeps partial-population tolerance and the empty-bindings rejection. Per-field leaf classification factors into a shared bindField returning a builder-internal sealed FieldResult (Ok / Fail); no new CallSiteExtraction leaf, so InputBeanInstantiationEmitter’s exhaustive switch is untouched, and `FieldBinding keeps sdlFieldName (the wire/Map key) separate from javaFieldName (the member) so emit stays selection-agnostic. Rejections ride the existing Built.FailRejection.structuralUnclassifiedField path. The @field docstring gains the Java-member axis for both the R191 output (FIELD_DEFINITION) and R200 input (INPUT_FIELD_DEFINITION) sites, paying off R191’s output-axis doc debt. The R97 org.jooq.* looksLikeBeanCandidate seam (jOOQ-TableRecord-as-param) and the R195 @nodeId jOOQ-record-member leaf are untouched. Coverage: pipeline-tier positive cases (record + JavaBean renamed via @field, asserting FieldBinding.javaFieldName() is the directive value while sdlFieldName() stays the SDL name), rejection cases (direction A/B, ambiguity, blank value, all on UnclassifiedField.reason()), a regression floor (a divergent-name JavaBean without @field still rejects "has no fields matching"); execution-tier submitFilmReviewSummary_routesThroughFieldRenamedRecordBean round-trips through a @field-renamed record bean (the round-tripped reviewId proves positional binding); new fixtures TestInputBeanRenamed / TestInputJavaBeanRenamed / TestInputSubsetRecord / FilmReviewSummary. Input-side counterpart of R191; R201 / R202 carry the remaining @field-symmetry items. Full reactor green.

  • R307 (Part A 2741787 + Part B 5fe85f5; In Review transition cce19f3): Retire stale @record references. @record stays a declared, legal-but-ignored directive (directives.graphqls declaration + readRecordClassName intact, nothing reads it to drive binding), but every treatment that implied it was live is gone. Part A rewrote the five rejection messages that steered authors toward authoring @record (dropping the never-valid @record(class:) form) to name the reflected-backing path instead, and renamed @record-as-jargon for "record-backed type" to "record-backed" / the variant name across main and test source. Part B: (1) the standalone TypeBuilder.emitDirectiveIgnoredWarnings post-classification re-walk is replaced by a per-type emitDirectiveIgnoredWarning called from the single classification pass, so the deprecation warning is a classification output; the three message variants (shadowed-by-@table, redundant/matches, disagrees) and the multi-producer-rejection suppression are preserved unchanged. (2) The LSP no longer treats @record as a live ExternalCodeReference-className binding: no className FQN completion, no "Unknown class" diagnostic, no live-binding hover, and (a warranted extension past the spec’s literal three surfaces) no legacy name:→className: alias nudge. Because the InputField("ExternalCodeReference", "className") coordinate is shared with @enum, each surface gates on the enclosing directive name (mirroring METHOD_VALIDATING_DIRECTIVES); the completion site gained the directive via a new directiveName field threaded from LspVocabulary.CursorLocation into CompletionContext. No @deprecated/deprecatedCoordinates wiring; the editor’s "ignored" signal stays the generator BuildWarning already surfaced through Diagnostics.validatorDiagnostics. (3) Every applied @record was purged from test-fixture SDL in both modules: generator binding-hint drops are classification-neutral (reflection binds via the @service/@mutation producer or @table), warning coverage consolidates into the new RecordDirectiveIgnoredWarningTest (three variants + suppression + reachability + @error-ignored + @table`@record+` no-conflict, all at the classifier with no generated-body string assertions), the LSP carve-out fixtures keep @record to assert the absence of tooling, and ValidatorDiagnosticsTest pins the @record-ignored BuildWarning surfacing as a usage-site Warning. BuildOutputReportPipelineTest’s report-wiring warning swapped from a redundant `@record to a redundant @splitQuery (the test needs only a model warning). The only applied @record left in either test tree is RecordDirectiveIgnoredWarningTest and the LSP carve-out fixtures. Out of scope (deferred): removing the directives.graphqls declaration. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules).

  • R264 (98bafe8; In Review transition ef7bd17): roadmap-tool status round-trip no longer strips quotes from front-matter titles. The status subcommand rewrote the block through a snakeyaml load-then-hand-serialize round-trip with no value-quoting, so a quoted title: containing ": " (the common "subtitle: detail" shape, e.g. R256) came back as a bare string and was re-emitted as invalid YAML; the very next parse, including the README regeneration the same subcommand runs, threw ScannerException and left the file unreadable. Replaced the lossy write path with patchFrontMatter, which rewrites only the named keys (status, last-updated) in place and leaves every other line, the body, and the fences byte-for-byte untouched, also removing the latent risk of the round-trip reformatting lists and dates. Routed the sibling writeChangelogNextId through the same helper, retiring its identical hand-serialization loop. Coverage: RoadmapTitleQuoteRoundTripTest runs a full status subcommand (including the regeneration that was the live crash site) over a colon-bearing quoted title and asserts byte-for-byte preservation plus a clean re-parse, with a direct patchFrontMatter unit test for present-key replacement and absent-key append.

  • R290 (5ebc52c slice 1 + 1227f0c slice 2 + 84a49f2 slice 4; In Review transition 38006ad; docs passes 84de71e + 6ceafb0): Forward note (R316, 2026-06-19): the carrier × intent × mapping model this entry describes was pivoted to (source, operation, target); the Carrier / Intent / Mapping types and the carrier() / intent() / mapping() accessors named below no longer exist. Read the dimensional vocabulary here as historical. Field-side dimensional slots, materialise carrier × intent × mapping on the field and dissolve the fused cross-product’s leaf-identity reads. R222 Stage 3. Slice 1: carrier() / intent() / mapping() land as three narrow accessors on OutputField (the field root that survives Stage 6), computed at classification time, reproducing exactly what R281’s throwaway LeafTupleAdapter reconstructed; the dimension enums Carrier / Intent / Mapping move into the model package, the adapter is deleted, and the classified-corpus harness builds its test-side DimensionTuple by reading the three accessors off the field (ClassifiedHarness line ~114). Three accessors not a neutral tuple, per "narrow component types" and "sub-taxonomies carry distinct information": each consumer reads exactly the axis it forks on (legality reads carrier, polarity reads the intent family, build-vs-consume reads mapping, re-fetch reads intent × mapping jointly). The triple is a total classification (every field has all three), so no No<Family> absence arm. Slice 2: ConstructorField dissolved as wrong-by-design, a @table parent constructing a @record/@service child from its own row, reachable only via self-referential test coverage. The classifier’s ResultType arm in FieldBuilder.classifyChildFieldOnTableType now rejects with an UnclassifiedField whose structural rejection GraphitronSchemaValidator surfaces as a build-time error; the leaf, its dispatch (IMPLEMENTED_LEAVES/FetcherEmitter/TypeFetcherGenerator/CatalogBuilder), and its LeafTupleAdapter arm are removed. The constructor corpus example leaves the classified corpus and becomes ConstructorFieldValidationTest’s rejection fixture; the `GraphitronSchemaBuilderTest verdict + @ProjectionFor sibling are deleted and the SingleRecordPayloadPipelineTest / DummyFetcherFixtures constructor-child fixtures removed. Live leaves 49 → 48. Slice 4: the re-fetch derivation made real, the proof the slots earn their keep. OutputField.requiresReFetch() is the single home of the service/DML → @table re-query predicate, derived from intent × mapping (mapping == Table AND intent in {QueryService, MutationService, Insert, Update, Upsert, Delete}) rather than re-decided per leaf in the consumer; GraphitronSchemaValidator.validateField mirrors it against the generator’s actual re-fetch dispatch (dispatchPerformsReFetch) so the single-homed predicate and the emitter cannot drift, per "validator mirrors classifier invariants". Slice 3 (the SingleRecordTableField → RecordTableField collapse) was split out to R305 once implementation showed it is an emit-mechanism unification, not a leaf merge; R290’s delivered leaf set is 48 (the appendix’s 47 is R305’s post-collapse target). The ChildField → SourceField carrier rename is split to R302. Coverage: ReFetchDerivationTest (behavioural assertions on the accessor and on validation output, no code().toString() body matches), ConstructorFieldValidationTest (retargeted to the build-time rejection), GeneratorCoverageTest.everyGraphitronFieldLeafHasAKnownDispatchStatus (reflection-driven exhaustive/disjoint dispatch partition, stays green with one fewer entry), the R281/R299 classified corpus (byte-identical modulo the one removed constructor example), and the compile + execution tiers against real PostgreSQL as the behavioural backstop. Full reactor green end-to-end.

  • R303 (fbe9ac6 + 642d67a + 3462bf8; docs-hygiene rework 6adebd5 + 7f98c68): Reify inline datafetchers into named <Type>Fetchers methods. Most generated DataFetcher`s were emitted as anonymous inline value expressions in `<Type>Type.registerFetchers (lambdas, the R244/R268 arm-switch ternary, the record-walking blocks) or bare new ColumnFetcher<>(column) instantiations, leaving a datafetcher with no named symbol to breakpoint, stack-trace, or look up by field. Now every datafetcher is a public static method on the corresponding <Type>Fetchers class and the registration site is uniformly <Type>Fetchers::<field>, for every owning object type (root, table, node, result, nested, connection, edge, @error). Seam: FetcherEmitter’s value-CodeBlock` contract is replaced by a sealed FetcherBinding (Inline | Reified) so the field-name-to-method-name derivation lives in one place and the registration value and method declaration cannot drift; bind returns Reified carrying both the MethodSpec and the registration value (a bare Fetchers::field for env-dependent reads, or new LightFetcher<>(Fetchers::field) for source-only reads), and TypeFetcherGenerator.generateTypeSpec collects the reified method alongside the existing variant switch (method-backed variants return Inline, so no double-emission; the dispatch partition is untouched). The light path is preserved by renaming the generated ColumnFetcherLightFetcher: it holds a Read<T> source-read SAM (T apply(Object source)) instead of a jOOQ Field<T>, stays a LightDataFetcher, and wraps the named read so the env-skipping fast path survives while the read gains a per-field symbol (the jOOQ column constant moves from the registration site into the method). The BatchKeyField-only gate for nested-type fetcher classes widens to "owns any fetcher" via one shared FetcherEmitter.nestedTypeOwnsFetchers predicate that both the reference site (FetcherRegistrationsEmitter.nestedBody) and the emit site (TypeFetcherGenerator.collectNestedFetcherClasses) call, closing the two-gate drift. Connection/edge get <Conn>Fetchers / <Edge>Fetchers delegate classes (ConnectionFetcherClassGenerator) whose thin per-field methods forward to the shared ConnectionHelper (one home for the pagination logic, hand-auditable; totalCount keeps its SDL-presence gate); @error types get <ErrorType>Fetchers with reified path / message reads (ErrorTypeFetcherClassGenerator) wired in place of the inline cast-lambdas in GraphitronSchemaClassGenerator. The R244/R268 Outcome arm-switch is reified to statement form (if (!(source instanceof Success<?> success)) return null; return ...;), the highest-value readability win and exactly the un-breakpointable expression the "Generated code is read and debugged" principle targets. One shape is honestly deferred: the @error PayloadAccessor errors field stays Inline(PropertyDataFetcher.fetching(name)) because reifying it needs a generation-time resolved accessor that ChildField.ErrorsField / Transport.PayloadAccessor do not carry, a classifier change this Spec scoped out; R304 (filed Backlog) carries the classifier-backed reification plus the resolvesViaPropertyDataFetcher / validateOutcomeChildArmSwitch reconciliation and the DataFetcherKind.PROPERTY_FETCHER retirement. Behaviour-preserving relocation: no classifier branch, no validator-mirror consequence. Coverage: FetcherPipelineTest wiring-kind + method-presence assertions (propertyField_onRecordType_reifiesReadMethod, propertyField_onBackedRecord_wrapsAccessorReadInLightFetcher, recordField_onRecordType_reifiesReadMethod, outcomePayload_columnDataField_armSwitchesInlineReadOnSuccessValue flipped to COLUMN_FETCHER-wrapping-a-method-reference), the per-type "class is emitted" pins for <Conn>/<Edge>/<ErrorType>/no-BatchKeyField-nested classes, TypeSpecAssertions.wiringFor matching new LightFetcher, and the compile-spec (sakila-example, <release>17</release>) + execute-spec tiers as the structural and behavioural backstops; no CodeBlock-string-equality assertions on reified method bodies. Two In Review -> Ready cycles of docs-hygiene rework swept the stale ColumnFetcher spelling and the inverted "no per-field fetcher method / emitted inline" invariant out of src/main javadoc per "Documentation names only live tests/code". Full reactor green end-to-end.

  • R284 (1e2e719 In Review follow-up; the original four-site pass predates distinct history, folded into the squashed trunk): Fix reversed source/target alias order in bridging-hop @reference ConditionJoin emission. An FK-first-hop-then-@condition bridging path emitted the two-arg condition-method call as (targetAlias, sourceAlias), violating R16’s fixed (srcAlias, tgtAlias) convention; with the documented opptak samordnaOrganisasjoner shape (concrete, mutually incompatible junction-vs-leaf jOOQ table types) the generated resolver fails to compile. The same reversed call was duplicated across five emission sites; the initial pass swapped four (InlineColumnReferenceFieldEmitter, InlineTableFieldEmitter, and SplitRowsMethodEmitter’s split-rows + connection-rows arms) and the In Review follow-up swapped the fifth, `InlineLookupTableFieldEmitter, which carried the byte-identical reversed arm and shipped unguarded. The defect shipped silently because every prior condition-join fixture declared generic Table<?> parameters, which compile either way. Guard: the new ReferencePathConditionFixtures.filmActorJunctionToActor(FilmActor, Actor) fixture takes concrete incompatible types, so any future re-reversal fails to compile in compile-spec. Coverage: execution GraphQLQueryTest.splitTableField_bridgingConditionJoin_returnsActorsPerFilm round-trips Film.actorsViaJunctionCondition (split-rows path); inline-lookup guard FilmInlineBundle.actorsByKeyViaJunctionCondition routes the same FK-then-bridging-@condition path through InlineLookupTableFieldEmitter (the fifth site). Full reactor green.

  • e6d213d (reframe) + cc18815 (impl) → 5e34fb7 (Spec → Ready) ; R299 (intention-classification-dimension): Forward note (R316, 2026-06-19): the carrier x intent x mapping model this entry migrated the corpus onto was itself pivoted to (source, operation, target); the @classified directive and DimensionTuple now carry the new axes. Read the vocabulary here as historical. migrate the R281 corpus from the two-axis (producer, mapping) verdict onto R222’s refined carrier x intent x mapping model, while the leaves are still intact and ahead of R290’s field-side materialisation. DimensionTuple becomes (carrier, intent, mapping); ProducerStep retires; new Carrier {Query, Mutation, Source} and the full-model Intent enum land, mirrored SDL-side in ClassifiedDsl.PRELUDE and checked by carrierMirrorsAdapterValues / intentMirrorsAdapterValues. LeafTupleAdapter reconstructs all three from leaf identity (carrier from the enclosing sealed type, intent from leaf + DmlKind, mapping as before); the switch stays exhaustive over OutputField, and the derived layer (FetchRelated / re-fetch / new-query / polarity) stays computed, never asserted. @classified migrates to (carrier:, intent:, mapping:) across every corpus fixture; everyDimensionValueIsExercised now covers Carrier + Intent with a known-gap allowlist (the five R222 model-completeness gaps plus upstream-rejected Upsert, mirroring NO_CASE_REQUIRED). `code-generation-triggers.adoc’s Field Classification section is rewritten to the three axes + derived layer + assert-vs-derive, the child-table / record-table examples now teaching the derived layer. Corpus-and-docs only: no generator, validator, or field-model change (those are R290).

  • fc03387 + 6ab5127 + cf8262e (impl) → 97fbc02 (In Review) ; R293 (build-warning-cleanup): clean up build-time warnings so a full mvn install -Plocal-db is warning-free under -Xlint:all -Werror, leaving only declared-out-of-scope environment lines (sandbox jOOQ PG-version mismatch, Maven’s own Guice/Unsafe JVM notes, the R294 BuildWarning-channel fixture advisory). Mechanical sweep (handwritten-source raw types / dangling javadoc / serialVersionUID / getType→getTypeOrNull / Charsets→StandardCharsets, lsp FFM @SuppressWarnings("restricted") + surefire --enable-native-access, maven-plugin descriptor link, junit-platform.properties test-jar exclusion) plus emitter fixes. The generated-code casts the spec slotted for narrowest-scope @SuppressWarnings were instead dropped: env.getArgument / env.getSource are <T> T, so a typed-LHS statement removes the cast via inference (the spec’s preferred step-1 over its mis-categorised example); @SuppressWarnings reserved for the genuinely-unchecked residuals ((List<X>) map.get(key) off Map<?,?>). cf8262e further replaced the record-carrier Outcome.Success<?> capture + unchecked success.value() cast with a checked instanceof Outcome.Success<element> pattern-match (Success<T> implements Outcome<T>), via a shared emitRecordSourceLocal helper. Guard: -Werror added to the parent pom’s global compilerArgs (every -Xlint:all category enforced, none excluded, documented escape hatch), inherited by sakila-example’s release-17 generated-source compile (the cross-module backstop); ratchet comment updated. jOOQ ambiguous keys resolved by disabling <implicitJoinPathsToMany> on the public.* codegen (no catalog consumer navigates those to-many path methods).

  • 2228b67…aa7a45e ; R281 (classification-test-dsl): classification test DSL, @classified spec-by-example. Replaces the doc-prose-plus-405-enum-row double specification of classification behaviour with an annotated SDL corpus that is the readable spec. Two test-only directives, @classified(producer: [ProducerStep!]!, mapping: Mapping!) on output fields and @classifiedType(as: TypeVerdict!) on types (enums ProducerStep / Mapping / TypeVerdict validated SDL-side via ClassifiedDsl.PRELUDE, never leaked into the production directives.graphqls), assert the two-axis dimensional verdict R222’s field pivot will adopt: producer (a pipeline of length ≤ 2: ∅ inline-correlate, or steps Query / Service / Dml) × mapping (Table / TableConnection / Column / Record / Field). The throwaway LeafTupleAdapter bridges today’s fused sealed leaves to those tuples via a compiler-exhaustive switch over OutputField that is R164’s leaf↔dimension truth table. ClassifiedHarness classifies each fixture with today’s classifier and compares; ClassifiedDslTest pins three coverage obligations (adapter totality compiler-enforced, every dimension value exercised, TypeVerdict mirrors GraphitronType’s non-failure leaves with a simple-name-uniqueness guard). `VariantCoverageTest was rewired so output-field and non-failure GraphitronType leaves are owned by ClassifiedCorpus.coveredLeaves() as the single source of truth, while input-field leaves stay on the GraphitronSchemaBuilderTest enum table and the failure leaves stay out of scope. QueryViewRenderer renders doc examples as query/fragment-as-view projections (real SDL regenerated, test directives stripped) with input-object and abstract-output-type closure expansion (pre-migration hardening item 3, QueryViewRendererTest); code-generation-triggers.adoc renders its worked examples from the corpus (ClassifiedDocTest) with reference tables corrected against TypeFetcherGenerator’s four-way emission partition (only `CompositeColumnReferenceField deferred). Retirement inventory committed at roadmap/audits/classification-test-dsl-inventory.md (35 pure-verdict rows, all retired against a corpus coordinate). Drives R222 Stage 3 (field-side pivot) as its executable acceptance spec; the TableInterfaceField / TableMethodField per-parent-query N+1 defect was filed as R288 rather than blessed as a [Query] verdict. Full reactor green.

  • fae7c6f + 1fdcf18 ; R295 (connection-synthesis-inherits-federation-tags): synthesised Connection / Edge / PageInfo types now inherit the federation @tag applications of their @asConnection carrier field, closing the contract-composition break where a tag-filtered contract kept the carrier field but dropped its untagged return type. ConnectionPromoter collects the arm-appropriate tags (carrier field on the directive arm, SDL Connection type on the structural arm), applies them to the synthesised Connection/Edge schema forms beside the existing shareable arm, and folds a tag union across all promoted carriers into the synthesised PageInfo exactly as pageInfoShareable folds shareable; an author-declared PageInfo is left untouched. Carriers sharing one connectionName: union their tags into the already-registered entry via typeRegistry.enrich on a transformed schemaType() (no parallel tags record component, per Model metadata over parallel type systems). Tests: ConnectionPromoterTest (explicit, repeatable, shared-name union, structural arm, SDL-PageInfo negative pin) + ConnectionFederationTagPipelineTest (<schemaInput tag> via loadAttributedRegistry and a federation-SDL emission round-trip). The deferred shareable boolean collapse is filed as R297. Tags land at the type level only; whether type-level-only tags satisfy a real Apollo contract build (vs. the field-level tags legacy contracts validated against) is the outstanding first-client check, tracked as R298 ; it could not run in the implementation/review sandbox, and the green SDL round-trip proves emission, not composition.

  • 5fa830e + 32d7e0d + 77573c4 (red tests 24387b2, comment refresh 905f9ef) ; R275 (source-record-carrier-service-error-channel): error channel and data projection for source-record-carrier @service mutations, reopened-scope completion. The earlier as-built slice (2026-06-05) closed only the to-one, non-@splitQuery carrier ({ entity: Table, errors } projected off Outcome.Success.value(), bucket C errors: null on the success arm, the NonNullableErrorsField rejection); this completion covers the two data-field shapes the opptak-subgraph saksbehandling mutations actually use, both of which previously emitted an invalid assembled schema (a typeRef to a dropped payload type, graphql.AssertException: type X not found in schema). Slice 1 (5fa830e): @splitQuery-list carriers ({ saker: [Sak!] @splitQuery, errors }) are admitted via the tolerant BuildContext.scanStructuralServiceCarrierPayload (the data field’s PK-keyed follow-up SELECT makes @splitQuery redundant, fired as the established warnIfSplitQueryOnRecordParent advisory), classifying SingleRecordTableField MANY over the OUTCOME_SUCCESS envelope; and a recognized-but-unbound orphan carrier becomes a loud UnclassifiedField at the mutation-field edge. Slice 2 (32d7e0d, requirement 2): @nodeId-from-record support ({ taggId: ID @nodeId, errors } / { tagger: [ID] @nodeId, errors } over a service returning the deleted record(s)) encodes node ids straight off Outcome.Success.value()’s in-memory record(s) with no follow-up SELECT, deletion-safe by construction; structural scan grew a named `CarrierFamily axis (DML vs SERVICE) carrying the forbidden-directive set and the ID-wrapper policy (SERVICE admits the [ID] list-of-nullable the opptak schema declares), the lockstep resolveDeleteIdEncoder + classifyDeleteIdEncoderError pair collapsed into one sealed IdEncoderResolution resolver feeding both DELETE and SERVICE diagnostics, and a new sealed leaf ChildField.SingleRecordIdField (wired through every sealed-coverage site: validator, TypeFetcherGenerator.IMPLEMENTED_LEAVES, FieldClassification.SingleRecordId, LeafTupleAdapter, LSP hover/label). Slice 3 (77573c4, requirement 1): the shape-agnostic GraphitronSchemaBuilder.rejectDanglingTypeReferences builder pass demotes any classified field whose SDL Object return element never registered to UnclassifiedField (fails the build and removes the field from emission), closing the residual hole for errors-only and scan-Reject orphans that the per-shape guard left open; the seven historically-lax arg-mapping fixtures were fixed (not allowlisted) by backing FilmDetails with a real TestFilmDetailsDto. Coverage: pipeline SingleRecordTableFieldServiceProducerPipelineTest, schema-builder GraphitronSchemaBuilderTest RootFieldCase + UnclassifiedFieldCase rows, execution GraphQLQueryTest delete-shaped fixtures whose producers synthesize records with ids absent from the DB (9001/9002) so the encoded node ids prove the no-re-fetch contract structurally; no generated-body string assertions. In Review → Done gate (reviewer session ≠ implementer session 017DpiWem9o8HCVkgDf7ae5a): full mvn -f graphitron-rewrite/pom.xml install -Plocal-db green on JDK 25 across all modules.

  • b2c0895 ; R291 (strip-internal-directives-from-published-sdl): the published SDL (schema.graphqls, both federation and plain arms) no longer carries Graphitron-internal directive definitions/applications or their supporting types. Two-tier support-type model derived from directives.graphqls (DirectiveSupportTypes): the published tier (SortDirection, now with SDL descriptions) classifies iff a non-support coordinate references it; the strictly internal tier never classifies, and a consumer reference to one rejects the referencing type with a typed AuthorError. The retention decision is schema.types() membership, consumed by both the runtime registration and the SchemaSdlEmitter print seam. Status flip and R253 closure at 80623b0.

  • b2c0895 ; R253 (pipeline-runtime-sdl-parity-test): closed as subsumed by R291 (strip-internal-directives-from-published-sdl), which implemented R253’s Route 3 at the SchemaSdlEmitter print seam (survivor-filtered directive definitions/applications on both arms, generateServiceSDLV2-mirroring federation printer) and re-enabled FederationBuildSmokeTest.emittedSdlMatchesRuntimeSchema as the pinning parity assertion. One changelog line records both IDs; R291’s own entry lands when it reaches Done.

  • ecdc7c4 ; R186 (nested-input-types-in-mutation-fields): a plain (non-@table) input object grouping columns of the surrounding @table input is now admitted on @mutation fields, flattening onto that one table instead of being structurally rejected. The grouping is a wire-format ergonomics shape with no DML semantics; the three structural rejections it replaced (UpdateRowsWalker / DeleteRowsWalker UnsupportedInputFieldShape, MutationInputResolver’s R128-attributed `NestingField arm) are gone. Both walkers flatten a NestingField into its leaf carriers in place and the INSERT resolver recurses its leaves under the same per-field rules; each nested leaf’s wire concern rides on a CallSiteExtraction.NestedInputField access path so the flat-leaf partition (UpdateRows.setColumns/keyColumns, DeleteRows.whereColumns, TableInputArg.lookupKeyFields) stays flat and the emitters descend the wire map. The emit honors the same absent-vs-null contract at every nesting layer that top-level mutation inputs do (absent / null group skips its subtree; a present group descends per leaf), proved on real PostgreSQL across INSERT, single + bulk UPDATE, and DELETE. List-typed nestings and nested-group @condition are rejected naming R186; nested @nodeId FK-targets (R189) compose; nested @table inputs that introduce a second DML target remain R122’s territory. New DML_INSERT_NESTING_OK flips the former DML_NESTING_FIELD_DEFERRED. Single-segment access paths emit byte-identically to pre-R186.

  • 57cb7b0 + f1ee7a6 ; R266 (deleterows-walker-carrier): DELETE mutations onto the DeleteRows walker carrier (sealed Identified | Broadcast), mirroring R246/R258’s UPDATE work for the DELETE verb. Row identification is catalog-derived PK-or-UK coverage via the shared MatchedKeys.firstCovered matcher both walkers call (the seam a future LookupRows carrier grows from); Identified’s matched key is a single-row guard, `multiRow: true opts into the Broadcast arm. New DeleteRowsField worn by the migrated MutationDeleteTableField (drops tableInputArg) plus the new MutationDeletePayloadField / MutationBulkDeletePayloadField; MutationDmlRecordField narrowed to {INSERT, UPSERT} and MutationBulkDmlRecordField to {INSERT} (compact-ctors reject DELETE). New DeleteRowsError sub-seal (NoUniqueKeyCoverage, UnsupportedInputFieldShape, OverrideConditionNotSupported) under graphitron.delete-rows.*. Carving DELETE off MutationInputResolver.resolveInput retired the @value directive entirely (absorbing R188): the declaration, DIR_VALUE, DmlKind.acceptsValueMarker / requiresPkCoverage, the valueMarkedNames partition machinery, and value.adoc are all deleted; mutation.adoc rewritten to the catalog-derived rule. Rework (f1ee7a6) closed the In Review feedback: aligned the BuildContext @lookupKey rejection message with the FieldBuilder twin, and shipped the execution-tier UK-covering single-row delete over a dedicated public-schema storage_bin fixture (deleteStorageBinByCode, WHERE on the UNIQUE code / RETURNING the bin_id PK, round-tripped against Postgres).

  • R8 (docs-as-index-into-tests, superseded by R279): closed as superseded wholesale rather than shipped independently. Steps 1-2 (re-sectioning groundwork, description normalisation) shipped earlier on claude/review-docs-plan-adYJW; step 5 was already retired by the variant-coverage meta-test (GeneratorCoverageTest.everyGraphitronFieldLeafHasAKnownDispatchStatus + VariantCoverageTest.everySealedLeafHasAClassificationCase). The remaining steps 3-4, positioning code-generation-triggers.adoc as a map into the GraphitronSchemaBuilderTest truth table, were deferred until the sealed hierarchy stabilised; that stabilisation is R279’s (field-first-classification-driver) own deliverable; the doc-as-index work (the code-generation-triggers.adoc absorption) landed in R279’s slice 0 and has since moved to R281 (classification-test-dsl), which now owns it and captures R8 by reference. Discarded per the workflow’s superseded-wholesale rule (the successor spec captures the predecessor); file deleted in this commit. Mirrors the R166 retirement precedent.

  • R259 (primary surface bundled in 048c9c7; sibling-namespace slice this commit): the @reference(key:) "did you mean" FK candidate hint is now scoped to the structurally relevant FKs and rendered in the namespace the author typed. The bug: the hint was built from catalog.allForeignKeySqlNames() ranked by global Levenshtein distance, so on a large schema the nearest five were dominated by unrelated FKs sharing a token, and an author who wrote keys in the jOOQ Java-constant TABLE__CONSTRAINT namespace got suggestions back in the bare SQL-constraint namespace. The primary surface, BuildContext.parsePathElement via the new fkCandidateNames(sourceSqlTable, attempt) helper (BuildContext.java:897), shipped both fixes: it scopes candidates to JooqCatalog.foreignKeysTouchingTable(...) (new helper, JooqCatalog.java:270, with a global fallback when the source table has no touching FKs) and switches namespace on attempt.contains("__"), rendering fkJavaConstantName vs SQL names. This close adds the cheap half of the sibling surface, BuildContext.unknownForeignKeyRejection (:1009, the @reference(key:) / @nodeId synthesis miss path): it now mirrors the same __-namespace detection, drawing from allForeignKeyConstantNames() vs allForeignKeySqlNames(), so both surfaces read in the author’s namespace. The harder half, scoping that sibling (its call sites pass only the FK name, no source table; scoping needs a table threaded through :1115 / :1347 / :1376 / :1889), is filed as R282 (fk-key-hint-sibling-scope). Pinned by JooqCatalogMultiSchemaTest.unknownForeignKeyRejection_mirrorsAuthorFkNamespace_inCandidateHint: against the nodeid fixture’s real FK, a bare-form attempt yields SQL-namespace candidates (no __) and a __-form attempt yields constant-namespace candidates (carry __), the latter failing under the pre-R259 always-SQL behaviour. Out of scope (per spec): LSP completion/hover arms and the FK-resolution logic itself. Full mvn -f graphitron-rewrite/pom.xml install -Plocal-db green on JDK 25. (Gate note: the primary surface was implemented by a prior session and reviewed independently here; the small sibling-namespace mirror was authored in this session, so that slice is self-reviewed, mechanically mirroring the already-reviewed fkCandidateNames pattern with the new test and the full build as the safety net.)

  • R254 (048c9c7, bundled): generated GraphitronSchema.java and every per-type *Type.java are now emitted as flat statement-per-element bodies (one schemaBuilder / b local plus one short statement per root type, additional type, scalar, directive, field, interface, possible-type, and applied directive) instead of a single fluent method-call chain whose depth scaled with schema size. The deep chain overflowed javac’s expression-attribution recursion during incremental compilation under `quarkus:dev on large schemas; flattening removes the unbounded depth. Non-trivial sub-values (synthesised scalars, directive definitions, applied directives, field/argument definitions) are factored into private static factory methods via HelperMethodSink, so emission sites are bare-name references. GraphitronSchemaClassGenerator.generate (:204-235) and ObjectTypeGenerator (object :135-148, interface :175-185, union :203-212, field-def sub-chain :240-269) carry the cutover; the AppliedDirectiveEmitter / directive-definition siblings were already statement-form. Pinned by @PipelineTier SchemaEmissionChainDepthPipelineTest: a maxChainDepth scanner asserts no emitted expression-statement exceeds depth 16 across a federation fixture and a deliberately oversized LARGE_SDL, with a scanner self-test (maxChainDepth_detectsLongChain builds a 21-segment chain and asserts it scores > 16, plus flat-statement = depth-1 cases) proving the bound would trip on a revert to the chained form; statementCountInGraphitronSchemaBuildBody_scalesWithSchemaSize pins growth. The bound is scanned at string level, not asserted as code-string equality on method bodies, with the carve-out documented per design principles. In Review → Done gate (reviewer session != implementer): full mvn -f graphitron-rewrite/pom.xml install -Plocal-db green on JDK 25. (Provenance: the implementation reached trunk bundled inside 048c9c7, whose message reads R244 Ready -> In Progress; the granular R254 stage commits were squashed in a rebase and are not reachable from trunk.)

  • R255 (048c9c7, bundled): fix duplicate column projection in generated $fields() methods (the RC-6 regression). When a type’s composite @node(keyColumns:) overlapped a sibling @field ColumnField on the same column (forced by federation @key + @override entity dispatch always selecting id), both classifier arms appended the same jOOQ TableField, projecting the column twice and spamming jOOQ "Ambiguous match" INFO logs on every fetched row. TypeClassGenerator’s `$fields() accumulator is now a LinkedHashSet<Field<?>> (:215) that dedupes by jOOQ Field identity while preserving projection order, returned as a List via an ArrayList wrap (:236) so the emitted surface is unchanged; the former if (!fields.contains(...)) guard collapses to a plain add under Set semantics (:227-234). Aliased .as(name) projections stay distinct (jOOQ caches one TableField per aliased Table), so only true duplicates fold. Pinned non-vacuously at the execution tier by FederationEntitiesDispatchTest (:435-464): a federated _entities query selecting both id and customerId (both → customer_id) captures the emitted SQL and asserts customer_id appears in the projection exactly once, which fails under the old ArrayList accumulator; @PipelineTier DedupeReferenceProjectionPipelineTest pins the classifier precondition (composite CompositeColumnField + sibling ColumnField over the same column). In Review → Done gate (reviewer session != implementer): full mvn -f graphitron-rewrite/pom.xml install -Plocal-db green on JDK 25. (Provenance: granular R255 stage commits were squashed in a rebase; the code reached trunk bundled inside 048c9c7.)

  • R260 (143b0c13): make generated NodeId-decode code readable and debuggable. Every NodeId-decoded condition argument (all key arities, skip and throw, scalar and list) now lifts into a private static decode<Type>Key/Keys/Row/Rows helper on the <Root>Conditions class via CompositeDecodeHelperRegistry, so the call site collapses to helper(wireExpr) (ArgCallEmitter.buildNodeIdDecodeExtraction, :360-389) and the helper body is statement form with meaningful locals (nodeId, key) instead of the former inline nested ternary with underscore pattern-locals and a Supplier-lambda-throw trick (CompositeDecodeHelperRegistry.buildHelper, :86-138). The sibling map/list traversal walkers were converted to map1 / list2 / elem3 bindings, and a registry-less decode now throws IllegalStateException rather than falling back to the old inline form (:378). Pinned by CompositeDecodeHelperRegistryTest (naming matrix, return types, projection and skip/throw bodies, including arity-1 coverage) and end-to-end by execution-tier GraphQLQueryTest.films_filteredByArgNodeId_dropsWrongTypeIdViaSkipHelper, which confirms a wrong-type id decodes to null and is dropped by the lifted helper’s filter(nonNull). The registry unit tests assert on helper-body substrings, defensible for an inherently emitted-shape item since behaviour is pinned separately at the execution tier and the assertions lean on structural API (helper.name() / returnType() / registry.emit() size). Out of scope (correctly excluded): the lookup-key and R195 input-bean decode-local paths. In Review → Done gate (reviewer session != implementer session 0182HAPCJwMRxopaPWwquKyk): full mvn -f graphitron-rewrite/pom.xml install -Plocal-db green on JDK 25.

  • R64 (retired as obsolete + residual dead-code deletion): the planned lift, making SplitRowsMethodEmitter.buildRuntimeStub accept a typed Rejection.Deferred / EmitBlockReason instead of a free-form String reason, is moot. The premise was that SplitRowsMethodEmitter.unsupportedReason returned Optional<Rejection.Deferred> and four buildFor* callers fed .message() into buildRuntimeStub; the rows-method rework that followed (the buildSingle/buildList/buildConnection cutover) deleted unsupportedReason and the four .message() call sites entirely, leaving buildRuntimeStub as a private, uncalled method (verified: zero call sites repo-wide, no unsupportedReason / Rejection.Deferred / EmitBlockReason reference anywhere in the file). There is nothing left to type-lift. This entry deletes that dead method (and its now-empty // Stubs section header), the actionable residue the obsolete plan left behind. Full mvn -f graphitron-rewrite/pom.xml install -Plocal-db green on JDK 25 across all tiers after the deletion. (Closure framing: retirement of an obsolete Backlog item plus a verified dead-code removal, not an In Review → Done approval of planned work; mirrors the R166 retirement precedent.)

  • R227 (048c9c7, bundled; retroactive close): mdBodyToAdoc translates markdown tables embedded in .md roadmap plans into AsciiDoc |=== blocks when staging plan bodies into the documentation site, closing the render-side hole R223 explicitly deferred (R223 only flagged the pattern in authored .adoc). The converter detects a markdown-table block (a MD_TABLE_ROW header line immediately followed by a MD_TABLE_SEP separator, then body rows until the first non-table line) and emits [cols="N*", options="header"] + |===, one cell per line. parseMdTableCells strips the conventional leading/trailing pipes, splits on unescaped pipes, unescapes \|, and leaves pipes inside backtick code spans intact (so `Map<K|V>` survives); each cell runs through the same bold / link / em-dash-sweep transforms as body prose, with literal pipes re-escaped for AsciiDoc. Coverage: MdTableToAdocTest (7 cases) pins the simple conversion + cols="2*" synthesis, in-cell bold/xref transforms, em-dash sweep, the backtick-pipe-protection corner, the code-fence non-conversion skip, and the parseMdTableCells strip/unescape edge cases. Scope cut from the spec: the [cols=...] attribute is synthesized from the column count only (equal-width N*); GFM alignment markers (:---:) are parsed by the separator regex but not carried into per-column alignment, since no roadmap .md table uses them and equal-width is the safe container-filling default. Provenance note: the implementation and MdTableToAdocTest were committed bundled inside 048c9c7 (whose message reads R244 Ready -> In Progress) and the item never transitioned out of Backlog; this entry records the retroactive In Review → Done close. Gate (reviewer session != implementer session 01YQEc4FG3cqf18pHwgVAnsX): implementation and test verified present in-tree and green on trunk.

  • R166 (Backlog, never specced; retired without a landing commit): graphqlschemavisitor-driven-emission superseded by R279 (field-first-classification-driver). R166 proposed a GraphQLSchemaVisitor-driven emission walk to fix per-emitter skip-filter drift (the R165 bug class) and the missing reachability sweep; R279 delivers both at classification time (the prune is structural, emission stays plain iteration over the pruned GraphitronSchema), so the visitor-driven emitter and R166’s standalone ReachabilityPruner alternative are both unneeded. R166’s emission-side open questions (cross-cutting aggregators, utility-class emitters, visitor ordering/determinism, visitor test ergonomics) evaporate with emission staying iteration-based. The one orthogonal sub-thread, the typed non-empty carrier for FetcherRegistrationsEmitter.emit’s `Map<String, CodeBlock> return (R166 Q7, originally R165), spun out to R280 (fetcher-bodies-nonempty-carrier).

  • R276 (468b86e + 8498e61 + 4ad9030 + 19681e5 + 9f53f36 + spec correction c69bce5): Record binding is reflection-only and sound. The four classifier/binding sites that still read @record to drive backing-class or kind are gone: RecordBindingResolver.groundServiceField’s `sdlHasRecord gate, TypeBuilder.classifyType’s `|| hasAppliedDirective(DIR_RECORD) arm, and the directive-className fallbacks in buildResultType / buildInputType. The service producer’s reflected return element now grounds the result observation through a shared groundProducerResult helper under a cardinality-match guard (single→single, list→list) plus the @table-backed-SDL and shouldBind guards, so a source-record-carrier payload (no @record) binds to its producer’s JooqTableRecordType rather than degrading to an unbound plain object; the R75 list-carrier path is preserved. @record stays a parseable, registered directive with the ignored-directive warning (emitDirectiveIgnoredWarnings / readRecordClassName), so existing schemas keep loading. D1: @record dropped from detectTypeDirectiveConflict (only @table vs @error remain mutually exclusive; @table/@error + @record warn instead of reject). The reopened completion scope made binding complete and sound: groundComputedField grounds @externalField / ChildField.ComputedField through the same shared helper, and propagateAccessorChains folds-then-cascades parent-accessor bindings (the root fix for the FilmCardWrapper.film / RecordExample.fieldC execution regressions). GraphitronType.PlainObjectType is eliminated as a terminal classification (Javadoc mention only): a genuinely unbound reachable object is routed to UnclassifiedType / left absent and surfaces at the field edge as UnclassifiedField (build-time failure, no silent runtime null), with the double-classification guard hardened. GraphitronType.PojoResultType.NoBacking and TypeClassification.UnbackedPojoResult are deleted (PojoResultType collapses to Backed), with every consumer updated. The spec-correction commit reverses a conflation: the LSP backing-shape projection TypeBackingShape.NoBacking.UnbackedResult is kept (it is load-bearing for eleven still-live GraphitronType variants in CatalogBuilder.projectType plus three LSP tests); only its stale Javadoc was refreshed. R157’s two LSP backing-shape fixtures migrated from @record(className:) to @service producers. Carve-outs: R277 (@tableMethod under a table-bound NestingField) filed to Backlog with its execution test @Disabled; R275 consumes R276’s carrier binding; the PlainObjectType / NoBacking deletions are the subtractive slice R222’s hierarchy cleanup builds on (boundary confirmed non-colliding, R222 still in Spec). Coverage: pipeline R96RecordBindingPipelineTest.unreachable_recordTypeIsIgnored_leftUnclassified + serviceListCarrier_bindsWrapperToJooqTableRecord, SingleRecordPayloadPipelineTest orphan-carrier case, R157PipelineTest LSP migration, the migrated GraphitronSchemaBuilderTest producer cases; no generated-body string assertions. In Review → Done gate (reviewer session ≠ implementer; widened scope re-confirmed at this gate per the spec’s own allowance): full mvn -f graphitron-rewrite/pom.xml install -Plocal-db green on JDK 25 across all eleven modules.

  • R214 (048c9c7, bundled; retroactive close): infer argMapping when a @condition / @service / @tableMethod Java signature is unambiguous, so authors no longer have to rename a Java parameter or write a redundant argMapping: "javaName: gqlName" for a structurally-unique pairing. ServiceCatalog.inferBindingsByType augments the name-based argByJavaName map after the override-typo check in two layered branches: arity-unique (exactly one unbound Java parameter and one unclaimed GraphQL slot, bound positionally only when the slot has no canonical Java mapping (named input object / enum) and the parameter is not a canonical scalar, so the unambiguousReachablePath dot-path suggestion still wins the scalar-into-wrapper case) and type-unique (each Java type appearing exactly once among unbound parameters and once among unclaimed slots is paired; asymmetric counts stay unbound and the existing diagnostic fires). Table<?>, DSLContext, context-key-named, and SOURCES-shape parameters (couldBeSourcesShape: List<RowN> / List<RecordN> / List<TableRecord> and Set<> equivalents) are held out of the candidate set so the per-parameter SOURCES classifier still wins at child coordinates. Threaded through every reflection caller that has slot types in scope (ConditionResolver, TableMethodDirectiveResolver, ServiceDirectiveResolver, and input-field @condition in BuildContext); the path-step @condition in resolveConditionRef has no slot types and is unaffected. Coverage: TestConditionStub.argConditionTypeUnique / argConditionTwoStrings fixtures plus ServiceCatalogTest.reflectTableMethod_typeUniqueSignature_infersBindingWithoutArgMapping (pins the inferred ParamSource.Arg binding, whatever → arg opptaksNavn) and reflectTableMethod_typeAmbiguousSignature_fallsBackToNameMatchingDiagnostic (pins the floor). Open follow-ups remain live items: inferred-binding provenance for the resolved-coordinate report / LSP (R218) and unifying the two branches under a JavaTypeKey-counted rule (R219). Provenance note: the implementation, fixtures, and resolver pass-throughs were committed bundled inside 048c9c7 (whose message reads R244 Ready -> In Progress) and the item never transitioned out of Backlog; this entry records the retroactive In Review → Done close. Gate (reviewer session != implementer session 01YQEc4FG3cqf18pHwgVAnsX): implementation verified present in-tree, and green on trunk through every Done item that landed on top (R244, R246, R195, R271).

  • R271 (07d6c23 + self-review abee3f1 + e6ec2d5): Retire the __-prefixed (dunder) Java locals/params/lambda-vars emitted across the generator (FetcherEmitter __r/__src/__fetched/__byPk/__ordered/__match/__key/__out/__ids; TypeFetcherGenerator __validator/__violations/__v plus the __arg_/__insertKey/__bulkKey/__lookupKey/__setKey/__bulkSetKey base prefixes; GeneratorUtils __elt/__k; InputRecordGenerator __c_/__e; ChannelEarlyReturnEmitter __violations) in favour of readable names (row, byPk, fetched, violations, element, key), with author-derived locals keeping a readable deterministic prefix (arg_<name>, c_<name>) rather than the dunder. The framing correction at the heart of the spec: the __ prefix was a lazy default, not a collision guard ; the generator emits every name in scope (signatures included), so a collision is knowable at generation time. The In-Progress audit confirmed no emitter places an author-derived parameter beside a generated local (all dunder locals live in DataFetcher lambdas, batch-loader/helper methods, or fromMap factories with generator-fixed signatures), so no disambiguation machinery was built. Genuine collision-avoidance names stay: synthetic SQL column aliases (__sort__/__idx__/__rn__/__typename/__pkN__) share the result-set column namespace with consumer-controlled table columns and remain __-wrapped, now promoted to named constants where they were bare repeated literals (RN_COLUMN/IDX_COLUMN on MultiTablePolymorphicEmitter and SplitRowsMethodEmitter) and documented with the DB-column-collision rationale at each constant; GraphitronSchemaClassGenerator’s synthetic-column `__typename routes through a named TYPENAME_COLUMN while the federation _entities representation-map __typename stays a literal (the GraphQL introspection meta-field, a distinct concept). The stale ChannelCatchArmEmitter javadoc naming __t/__m (the code emits mapping/cause) is fixed. The standing rule lands in rewrite-design-principles.adoc. No-regression guard ships in two tiers, keying on the Java-identifier-vs-string-literal discriminator (both mask comments + string/char literals before scanning): @PipelineTier DunderFreeEmissionPipelineTest (in-process TypeSpec scan, non-vacuous by an isNotEmpty assertion) and @CompilationTier GeneratedSourcesLintTest.emittedSourcesHaveNoDunderIdentifiers (full Sakila generated-sources walk with a ≥20-file floor against a vacuous pass). External tokens we do not emit (__NODE_*, federation__*, link__*) are out of scope. In Review → Done gate (reviewer session ≠ implementer): full mvn -f graphitron-rewrite/pom.xml install -Plocal-db green on JDK 25; both meta-tests run (not skipped) and pass.

  • R195 (7f7cac0 v1 + rescope 876a621 + composite/list ef8fe80 + c3e10a3 + type-mismatch gate 18f79a2 + rework 36712f7 → 98b5a93): decode @nodeId(typeName:) into jOOQ-*Record-typed @service input-bean member fields instead of miscompiling to a wire-String → *Record cast (the R150/R195 ClassCastException family). InputBeanResolver.buildInputBeanBody now branches before else -> new Direct() when a member’s element Java type is assignable to org.jooq.Record: with a resolvable @nodeId(typeName:) it classifies to a new CallSiteExtraction.NodeIdDecodeRecord leaf carrying (encoderClass, typeId, keyColumns, TableRef table, nonNull); otherwise it is a typed Result.Failed, so a jOOQ-record member never falls through to Direct again. InputBeanInstantiationEmitter (the one reusable emitter) emits per-type concrete helpers (decode<Type>Record(Object) -> <Type>Record, deduped by record type, plus a decode<Type>RecordList stream variant for list members) whose body calls the now-public NodeIdEncoder.decodeValues(typeId, nodeId) then loads values positionally with decoded.fromArray(values, Tables.<T>.<col>…) ; one call regardless of key arity, coercing through the column converter, with no throwaway RecordN, no fromMap(intoMap()) round-trip, and no deprecated-for-removal DataType.convert(Object) (so no @SuppressWarnings leaks into the consumer’s *Fetchers; the encoder’s own decode<Type> convert is tracked separately as R267). All shapes ship: single-key, composite-key, scalar, list, and the list-of-composite corner. Loud rejections are malformed-directive-only (no @nodeId, missing typeName:, unknown NodeType) plus a member-type-vs-@nodeId-@table mismatch gate (18f79a2) that fails generation rather than emitting Tables.<NodeTable>.<col> references for the wrong record. ServiceMethodCallWalker carries the leaf through the R238 ValueShape re-projection unchanged; ArgCallEmitter gets an explicit unreachable arm. Coverage: pipeline NodeIdRecordInputBeanPipelineTest (9 tests, structural only ; helper presence by name, signatures, and a decodeRecordLeaf walk down the classified model asserting NodeIdDecodeRecord typeId/key-column arity/record class/nonNull, plus malformed-directive and type-mismatch Rejection.message() cases; no code().toString() body matches, per rewrite-design-principles.adoc line 131); compilation tier graphitron-sakila-example (single/composite/list/list-of-composite fixtures compile their fromArray field references against the real jOOQ catalog); execution tier GraphQLQueryTest (round-trips each shape against PostgreSQL, plus assignFilmRecord_wrongTypeNodeId_throwsDecodeMismatch pinning the throw-on-mismatch contract behaviourally). Original R195 framing (top-level @service parameter that is a jOOQ record, @field(name:)/@table-on-input translation) deferred (tangled with R97). In Review → Done gate (reviewer session ≠ implementer/prior-reviewer): full mvn -f graphitron-rewrite/pom.xml install -Plocal-db green on JDK 25.

  • R246 (8a04c0b + 975f593 + c63c14c + rework f3a39ea): UpdateRows walker carrier (R222 UPDATE slice) with PK-or-UK identification. @mutation(typeName: UPDATE) returning its @table type directly (or ID) now classifies through FieldBuilder.classifyUpdateTableField → UpdateRowsWalker instead of MutationInputResolver. MutationUpdateTableField drops its tableInputArg component (and tableInputArg() comes off the DmlTableField sealed parent; INSERT/DELETE/UPSERT keep their own) and gains two non-Optional slots via the new UpdateRowsField interface: a slim InputArgRef (SDL arg name, input type name, jOOQ TableRef, list flag) built directly by FieldBuilder, and an UpdateRows carrier (sealed, one Identified arm whose compact constructor enforces non-empty setColumns) holding the MatchedKey (PrimaryKey/UniqueKey) plus the SetColumn/KeyColumn partition. The load-bearing claim is PK-or-UK identification: the walker reads jOOQ Table.getPrimaryKey()/getKeys() via JooqCatalog.candidateKeys (PK-first, deduped), matches the first key whose column set is a subset of the input-covered columns, and partitions input fields into WHERE (matched-key) and SET (everything else) halves. multiRow: true on UPDATE is rejected outright at the FieldBuilder pre-check (Rejection.deferred, empty slug, no follow-up planned) ; broadcast UPDATE has no replacement path; covering a PK/UK is the single-row UPDATE shape. The emitter cutover is carrier-driven in place (not a separate UpdateRowsEmitter class): buildMutationUpdateFetcher/buildBulkUpdateFetcher project setColumns()/keyColumns() back into the SetGroup/InputColumnBindingGroup shapes via setGroupsOf/keyGroupsOf, emitting byte-identical SQL. Error taxonomy ships as a sibling sub-seal UpdateRowsError implements Rejection.AuthorError (five arms: NoUniqueKeyCoverage, NoSetFields, MixedCarrierKeyMembership, UnsupportedInputFieldShape, OverrideConditionNotSupported), each with lspCode() under graphitron.update-rows.*, wired into the LSP projector + typed-rejection.adoc + RejectionSeverityCoverageTest. R215’s classify-time admission of @condition(override: true) on UPDATE input fields inverts to a typed walker rejection (the filter was never emitted). Absorbs R146 (PK-or-UK coverage, discarded) and R188’s UPDATE-side partition scope; built as a translator over the already-classified InputField permits rather than raw-SDL re-derivation (the R238-style substrate concession, follow-up filed as R257). Coverage: unit UpdateRowsWalkerTest (11 cases); pipeline GraphitronSchemaBuilderTest (typed-arm migrations + R246_UPDATE_MULTIROW_TRUE_DEFERRED + R246_UPDATE_ARG_CONDITION_STRUCTURAL_REJECTED + R215 inversion); execution via the existing updateFilm round-trip (UK-driven execution case deferred, justified in as-built notes). In Review → Done gate (reviewer session ≠ implementer/prior-reviewer): full mvn -f graphitron-rewrite/pom.xml install -Plocal-db green on JDK 25 (graphitron, graphitron-lsp incl. RejectionSeverityCoverageTest, and the execution tier all pass).

  • R250 (73e7670 + 39072a3): GraphitronSchemaClassGenerator.generate now emits .withSchemaAppliedDirectives(java.util.List.of(...)) on the runtime schemaBuilder, immediately after the additionalDirective(survivors) loop and before .codeRegistry(...), so the consumer’s extend schema @link(url:..., import:[...]) propagates into the generated runtime build. Pre-R250 the schema-applied list was lost: GraphQLSchema.newSchema() starts empty and .additionalDirective(...) only emits directive definitions, not applications; the symptom in one consumer’s deployment was supergraph composition (@apollo/federation-internals completeSubgraphSchema) failing to detect Fed2 because the schema @link(...) block was missing from the runtime SDL, falling through to completeFed1SubgraphSchema and rejecting the canonically Fed2-shaped @key declarations with "argument fields should have type _FieldSet! but found federation__FieldSet!`". `AppliedDirectiveEmitter.applicationsForSchema(GraphQLSchema) is the new entry point; the schema-applied list is not a GraphQLDirectiveContainer in graphql-java so the helper takes the raw GraphQLSchema rather than reusing applicationsFor. Shape contract diverges from the per-container sibling on purpose and is documented inline: applicationsFor returns blocks pre-wrapped in .withAppliedDirective(...) because the per-type builders take one application at a time; applicationsForSchema returns bare GraphQLAppliedDirective.newDirective()...build() blocks because GraphQLSchema.Builder#withSchemaAppliedDirectives takes a single List<GraphQLAppliedDirective>. Survivor filter mirrors the per-container path; generator-only directives are skipped. Argument-value rendering routes through the same ValuesResolver.valueToLiteral + AstPrinter.printAst + Parser.parseValue chain the per-type emitter already uses, so @link’s `import: ["@key", ...] round-trips through an AST list literal without per-shape coding. The link__Import scalar and link__Purpose enum referenced by @link’s argument types are already registered on the runtime schema by R248’s `ScalarTypeResolver Synthesised arm and standard enum registration, so emitInputType’s `GraphQLTypeReference.typeRef("link__Import") resolves at schema-build time. Coverage: unit-tier AppliedDirectiveEmitterTest.applicationsForSchema_emitsBlocksForSchemaLevelSurvivorDirectives and applicationsForSchema_skipsGeneratorOnlyDirectives pin the helper output for a schema-applied @link(url:..., import:["@key"]); unit-tier GraphitronSchemaClassGeneratorTest.build_emitsWithSchemaAppliedDirectives_forSchemaLevelLink pins the emitted call site and its position relative to .codeRegistry(...), with build_skipsWithSchemaAppliedDirectives_whenNoSchemaLevelSurvivors as the negative arm; pipeline-tier FederationBuildSmokeTest.serviceSdlExposesSchemaAppliedFederationLink builds the full sakila federated schema, queries _service { sdl }, and asserts the printed schema { ... } block carries @link(url:"https://specs.apollo.dev/federation/v2.10", import:["@key"]), locking the round-trip consumer SDL → applicationsForSchema → withSchemaAppliedDirectives → Federation.transform → _service.sdl against silent removal. The R247 file-emission side gains a sibling assertion in SchemaSdlEmissionTest that the federated file artefact carries schema @link(, closing the loop on the file the supergraph composer actually reads. Out of scope, follow-up filed: multi-federation-@link consumer schemas (FederationLinkApplier already rejects more than one federation @link with a developer-readable error); re-evaluating whether Federation.transform(base).setFederation2(true) is the right runtime wrap (separate concern, entity-resolver wiring is independent); multi-file federation fixture coverage for R247’s file emission (R252, filed). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R247 (73e7670 + 39072a3): New SchemaSdlEmitter runs at the tail of GraphQLRewriteGenerator.runPipeline and renders the assembled GraphQLSchema to target/generated-resources/graphitron/<outputPackage as path>/schema.graphqls; GenerateMojo registers that directory via project.addResource(...) so maven-resources-plugin copies it into target/classes, shipping the file at <outputPackage as path>/schema.graphqls in the consumer’s JAR. The federation arm runs Federation.transform(assembled).setFederation2(true).build() before ServiceSDLPrinter.generateServiceSDLV2, mirroring the consumer’s runtime build so _Service / _entities / _Entity are present on both sides; the non-federation arm uses graphql-java’s SchemaPrinter with includeDirectives(true) / includeScalarTypes(true) / includeIntrospectionTypes(false) / includeSchemaDefinition(true). RewriteContext gains an outputResourcesDirectory record component; AbstractRewriteMojo.resolveOutputResourcesDirectory(basedir) derives it from project.getBuild().getDirectory() with a basedir/target fallback for hand-built MavenProject test fixtures (no @Parameter, no per-consumer toggle); after the self-review followup runGenerator returns the RewriteContext so GenerateMojo.execute reads outputDirectory and outputResourcesDirectory from one derivation site. Tests: unit-tier SchemaSdlEmitterTest (federation + non-federation + empty-package arms); pipeline-tier SchemaSdlEmissionTest (federated SDL carries the canonical @key, the synthesised federation__FieldSet scalar, the @link directive declaration, and the schema-applied @link(...) block; non-federation SDL parses through SchemaParser and is missing the federation surface; classpath resource lookup non-null under both outputPackage`s); `GenerateMojoTest.buildContext_derivesResourcesDirectoryFromBuildTarget locks the Maven-convention path against the hardcoded relative segment. Implementation folded the R250 work in per the user’s "ship together" directive (covered separately on R250’s gate). Out of scope, follow-up filed: the pipeline ↔ runtime SDL parity test (FederationBuildSmokeTest.emittedSdlMatchesRuntimeSchema, SchemaDiffing-based) landed @Disabled; closing the remaining non-survivor directive-definition / -application diff is tracked as R253 (Backlog), with Route 1 / 2 / 3 routes laid out. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R251 (1c1b425): AppliedDirectiveEmitter.buildApplication now skips arguments whose getArgumentValue().isNotSet() is true rather than feeding the NOT_SET slot into ValuesResolver.valueToLiteral (which assertShouldNeverHappen`s on it). The reconstructed `GraphQLAppliedDirective carries only explicitly-supplied arguments; consumer-side schema build resolves the rest from the directive definition’s declared defaults, matching graphql-java’s own applied-directive round-trip. Coverage: unit-tier AppliedDirectiveEmitterTest.omittedArguments_areSkipped_notRenderedAsNotSetLiteral builds @audit(reason: "pii") on a directive declaring both reason and ticket and asserts ticket does not appear in the emitted .argument(...) chain. R248 fixed the adjacent directive-definition-side bug; R251 closes the application-side symmetry.

  • R248 (a2b1705): DirectiveDefinitionEmitter now round-trips argument default values (.defaultValueProgrammatic(...) emitted via the same GraphQLValueEmitter.emit path ObjectTypeGenerator.buildArgument already uses for field arguments), so directive @key(resolvable: Boolean = true) survives the JavaPoet reconstruction with its default intact. The federation-namespace scalar fix is a sub-taxonomy lift on ScalarResolution: a new sealed Successful interface (javaType() accessor) sits between the root and Resolved, with a sibling Synthesised arm carrying (javaType, sdlName, coercingSourceOwner, coercingSourceField) for scalars that have no public static final GraphQLScalarType constant on the consumer classpath. ScalarTypeResolver.resolveFederationNamespaceScalar returns Synthesised(String.class, "federation__FieldSet", _Any, "type") instead of the old Resolved(String, Scalars, "GraphQLString") placeholder; GraphitronType.ScalarType.resolution widens from Resolved to Successful; TypeBuilder’s federation-namespace branch and Java-type registry lookup narrow to `Successful; TypeBuilder.asRejection switches on Successful (every successful arm throws ; only rejections reach the dispatcher). AppliedDirectiveEmitter.emitInputType emits GraphQLTypeReference.typeRef(name) for federation-namespace scalars instead of Scalars.GraphQLString, so directive-definition and applied-directive argument slots both reference the synthesised scalar by name. GraphitronSchemaClassGenerator.build()’s scalar-registration loop dispatches on the variant: `Resolved emits the existing .additionalType(Owner.FIELD), Synthesised emits an inline .additionalType(GraphQLScalarType.newScalar().name(<sdl>).coercing(<owner>.<field>.getCoercing()).build()); _Any.type.getCoercing() is the same lever federation-jvm uses in ensureFederationV2DirectiveDefinitionsExist when synthesising missing federation scalars at the registry+wiring entry point. The misleading "federation-jvm transform() replaces the placeholder after the base schema is built" comments at ScalarTypeResolver.java:83-95 / :310-314, TypeBuilder.java:601-605 / :647-651, and AppliedDirectiveEmitter.java:122-134 retire ; Federation.transform(GraphQLSchema) only adds _Any / _Entity / _Service and wires entity resolution, never rewrites @key or injects scalars; the divergence reached the printed Service SDL untouched. Coverage: pipeline-tier FederationBuildSmokeTest.serviceSdlExposesCanonicalKeyDirectiveShape asserts the printed SDL carries directive @key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE and scalar federation__FieldSet (the end-to-end behavior subgraph-composition tooling validates against); DirectiveDefinitionEmitterTest extends the existing argument-emit test with a .defaultValueProgrammatic( + "strict" assertion and adds a Boolean-default arm (@flag(enabled: Boolean = true)) to cover the GraphQLValueEmitter.emit dispatch on a different value shape; ScalarTypeResolverTest flips the federation-namespace resolver test to expect Synthesised(_Any, "type") and adds a second arm on link__Import to confirm the dispatch isn’t FieldSet-specific; GraphitronSchemaBuilderTest narrows existing ScalarType.resolution() reads to the Resolved arm they’re testing. Out of scope (called out): argument-level @deprecated on directive definitions (no survivor directive Graphitron emits today carries one); re-emitting @link itself (the federation library injects @link into the registry via LinkDirectiveProcessor, the survivor walker picks it up like any other directive); switching graphitron’s federation entry point from Federation.transform(GraphQLSchema) to the registry+wiring overload (would forfeit the prebuilt-programmatic-schema fast-boot model the rewrite chose in R10’s predecessor landing); federation v1 surface (FederationDirectives.key / _FieldSet.type / ensureFederationDirectiveDefinitionsExist) since FederationSpec.URL pins v2. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R237 (96869ab + e24feac + ee9720f + 246b349 + aa83a16 + 8845717 + 18a37a2): Retire the @LoadBearingClassifierCheck / @DependsOnClassifierCheck annotation pair and the LoadBearingGuaranteeAuditTest audit infrastructure. 183 annotation blocks stripped across 50 Java files; the four annotation classes (LoadBearingClassifierCheck, LoadBearingClassifierChecks, DependsOnClassifierCheck, DependsOnClassifierChecks) and the audit test + auditfixture/ package deleted. Phase 2’s four-bucket classification of the ~59 active producer/consumer keys found |c-signal|=0: every cross-module producer-consumer pair was already mechanically pinned by graphitron-lsp tests (FieldCompletionsTest, HoversTest, DiagnosticsTest, DeclarationHoversTest, ValidatorDiagnosticsTest) or by structural type narrowing on the producer side, leaving no signal-bearing contract that needed a test-side replacement. Phase 3 picked Delete on that strength; two follow-up Backlog items track the structural type-system lifts that retire the residual producer-consumer linkages mechanically rather than via documentation: R239 (column-field-requires-table-backed-parent, b-cheap, single-record parentTable lift to ColumnField record component) and R240 (service-catalog-strict-tablemethod-return + tablemethod-resolver-return-is-table-bound, b-relational, type-token threading on MethodRef.StaticOnly × ReturnTypeRef.TableBoundReturnType). Knock-on: PkResolutionEmitterReachabilityTest.classifyDeleteTableProjectionWearsLoadBearingClassifierCheckPin retired with its annotation dependency (the sibling sealed-arm symmetry test still pins the rejection contract structurally); residual javadoc references rephrased across the rewrite tree in two waves: the initial Phase 5 sweep (aa83a16 + 8845717) covered 11 main-source files, 4 test files, and 18 roadmap-item plan bodies; a self-review follow-up sweep (18a37a2) caught a further 15 main-source, 6 test, 1 schema, and 1 docs site still carrying the retired load-bearing classifier check {key} framing ; each rephrased to anchor on the actual structural pin (sealed-variant arm, compact-constructor invariant, non-null record component, named resolver class) rather than the retired key. rewrite-design-principles.adoc Phase 1 rewrite shipped at 96869ab anchored the principle on the three surviving layers (type-system narrowing at producer, pipeline-tier tests, cross-module compile against graphitron-sakila-example); the principles-architect agent and srp / reviewer-prompt skill rubrics swapped the "Load-bearing classifier checks" rubric for "Missing type-system lift". Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R233 (01c0172 + d02859f): LSP @field(name:) completion + hover arms now resolve the column against the @reference path’s terminal table instead of the enclosing type’s @table, closing the bug R224 fixed for diagnostics but not for the two sibling LSP surfaces. FieldClassification gains a sealed nested LspColumnDispatch (permits Resolve(tableName) / Silent / FallThrough) and an exhaustive lspColumnDispatch() default method that switches over all 30 sealed permits with no default arm ; a new permit fails the switch to compile, forcing one deliberate placement before any consumer-side switch. The four column-bearing permits (Column / ColumnReference / CompositeColumn / CompositeColumnReference) produce Resolve(tableName) carrying R224’s already-projected terminal table; InputUnbound / Unclassified produce Silent; every other permit produces FallThrough. The three consumer sites (Diagnostics.validateFieldMember, FieldCompletions.completionsFor, Hovers.columnHover) collapse to a uniform 3-arm switch on the projection: Resolve and Silent return directly, FallThrough drops through to the existing backing-driven dispatch. FieldCompletions switches from TypeContext.enclosingFieldDefinition to enclosingFieldOrInputValueDefinition (R224’s helper) so input-side input_value_definition nodes resolve too; Hovers.columnHover adopts the same helper. A small mergeWithSigil helper in FieldCompletions shares the $source-sigil merge between the new dispatched arm and the existing backing arm. Annotation hygiene: field-classification-payload-faithful now has five consumer sites (InlayHints.compute, DeclarationHovers.compute, Diagnostics.validateFieldMember, FieldCompletions.completionsFor, Hovers.columnHover); the producer description at CatalogBuilder enumerates the five consumers and names lspColumnDispatch() as the routing primitive. Tests: pipeline-tier LspColumnDispatchProjectionTest drives the full classifier on a synthetic schema and pins the three arms (Resolve / Silent / FallThrough) plus a cross-permit invariant that ColumnReference.tableName() == Resolve.tableName(); FieldCompletionsTest gains three R233 regressions parallel to R224’s DiagnosticsTest cases (inputTableWithReferencePathCompletesTerminalTableColumns, outputTableWithReferencePathCompletesTerminalTableColumns, unresolvedReferencePathCompletionSilentOnLspSide); HoversTest gains three symmetric regressions (inputTableWithReferencePathHoversOnTerminalTableColumn, outputTableWithReferencePathHoversOnTerminalTableColumn, unresolvedReferencePathHoverSilentOnLspSide); R224’s three DiagnosticsTest regressions stay green untouched. Self-review cleanup (d02859f) inverted the spec’s double-Optional-of-Optional dispatch shape at the two new consumer sites to direct returns (Resolve and Silent each return directly, FallThrough drops through), preserving the exhaustiveness guarantee while removing the nested generic. Out of scope (called out, filed as R236): the runtime-side BuildContext.classifyInputFieldInternal candidate hint at BuildContext.java:1673 draws its "Did you mean" suggestions from the path-origin table rather than the terminal table ; different surface (compile-time validator message vs. interactive LSP) and audience, owned by a sibling Backlog item. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R232 + R129 absorption (3079b99 + 001eee4 + 41e20a3 + 4bb9d4f + 70152d3 + 8effcbf): @reference(path: [{condition: {…}}]) paths now classify and emit a real correlated subquery / split-rows SELECT, replacing the build-time deferred-rejection that previously short-circuited the six condition-join-affected ChildField variants (TableField, LookupTableField, SplitTableField, SplitLookupTableField, RecordTableField, RecordLookupTableField) plus the seventh sibling ColumnReferenceField. JoinStep.ConditionJoin’s record header gains a `TableRef targetTable resolved at parse time by BuildContext.resolveConditionJoinTarget ; terminal hop from the carrier field’s return-type @table binding, intermediate hop by reflecting on the condition method’s second parameter type via JooqCatalog.findTableByClass ; with a compact-constructor null-check as the structural safety net behind the new condition-join.target-table-resolved-at-parse load-bearing-classifier key. The WithTarget capability splits into a smaller HasTargetTable (target read only) plus the slot-iteration WithTarget extends HasTargetTable, so ConditionJoin joins FkJoin / LiftedHop under one capability for the alias-declaration loop and JoinPathEmitter.targetJavaClassName collapse. A new sealed ParentCorrelation taxonomy (OnFkSlots / OnConditionJoin) lifts the step-0 fork between FK-slot correlation and ConditionJoin-method correlation out of every emitter site into the model, threaded through each affected ChildField variant’s record header with a parentCorrelation.firstStep() == joinPath.get(0) compact-constructor invariant. The inline emitters (InlineTableFieldEmitter, InlineLookupTableFieldEmitter, InlineColumnReferenceFieldEmitter) and the split-rows emitter (SplitRowsMethodEmitter’s `buildListMethod / buildSingleMethod / buildConnectionMethod) read the dispatch off the carrier; FK hops emit .join(alias).onKey(FK), condition hops emit .join(alias).on(method(prev, this)). For split-rows + OnConditionJoin, the prelude declares a fresh parentAlias Table local for the @table-bound parent, emits the step-0 .join(parentAlias).on(condition(...)) clause, and routes parentInput to JOIN on parent-PK columns. Validator-side: validateVariantIsImplemented’s `SplitRowsMethodEmitter.unsupportedReason consult and validateColumnReferenceField’s `hasConditionJoin branch both delete; validateReferenceLeadsToType folds its WithTarget special-case onto a uniform HasTargetTable read. The ConditionJoinReportable capability interface deletes outright; Rejection.EmitBlockReason enum (six values, all condition-join-step) and Rejection.StubKey.EmitBlock record retire with their last producers; JoinPathEmitter.hasConditionJoin predicate retires with its last consumer; docs/manual/reference/diagnostics-glossary.adoc’s six `=== <variant>-condition-join-step headings delete per DiagnosticsDocCoverageTest. R129 absorption: the column-reference-on-scalar-field-condition-join slug closes ; ColumnReferenceField with a multi-hop path containing a condition step now classifies and emits via InlineColumnReferenceFieldEmitter; a single-hop condition-only path on a scalar return type AUTHOR_ERRORs at the parser with actionable rewrite guidance (use {table:} or {key:}), which is the same diagnostic shape the deferred-rejection used to surface, now produced one stage earlier. Tests. Pipeline-tier *ValidationTest cases flip from deferred-rejection to no-error; new HasTargetTableInvariantTest pins the JoinStep-permits-implement-HasTargetTable invariant; ParentCorrelationFirstHopInvariantTest exercises both OnFkSlots and OnConditionJoin arms end-to-end; GraphitronSchemaBuilderTest gains CONDITION_ONLY_TERMINAL_RESOLVES_TARGET_FROM_RETURN_TYPE, TABLE_WITH_CONDITION_PRESERVES_WHERE_FILTER, KEY_WITH_CONDITION_PRESERVES_WHERE_FILTER (the last two are regression guards for the legacy {table:, condition:} / {key:, condition:} whereFilter-fold semantics), CONDITION_ONLY_NO_RETURN_TYPE_TABLE_REJECTED (AUTHOR_ERROR when the terminal-hop carrier’s return type has no @table), CONDITION_INTERMEDIATE_REFLECTS_METHOD_PARAM (reflection on the condition method’s second parameter type resolves the intermediate-hop targetTable), and CONDITION_INTERMEDIATE_TABLE_WILDCARD_REJECTED (AUTHOR_ERROR when the intermediate condition method uses Table<?>); plus extends the existing WITH_CONDITION_PATH fixture with a non-null targetTable() check. Symmetric finish on RecordTableMethodField: the seventh @record-parent variant gains a ParentCorrelation field on its record header so a {condition:}-first path AUTHOR_ERRORs at parse time (same shape as the sibling RecordTableField / RecordLookupTableField variants ; @record-parents have no @table to anchor the condition method’s source arg, so the synthesis routes through AuthorError); the dead instanceof JoinStep.FkJoin arm in SplitRowsMethodEmitter.buildForRecordTableMethod’s `unsupportedPath predicate retires, leaving only the pre-existing R43 limits (empty + multi-hop). Compile-tier + execution-tier: Sakila gains two condition-method fixtures, Customer.addressByCondition: Address @reference(path: [{condition: ...}]) exercising the inline TableField emission shape and Film.actorsByCondition: [Actor!]! @splitQuery @reference(path: [{condition: ...}]) exercising the split-rows emission shape via an EXISTS-over-junction predicate; ReferencePathConditionFixtures ships the two condition methods. Compile-tier coverage flows through mvn install (generated code compiles against the real jOOQ catalog); execution-tier GraphQLQueryTest.inlineTableField_conditionJoin_returnsAddressPerCustomer and splitTableField_conditionJoin_returnsActorsPerFilm assert end-to-end SQL correctness against PostgreSQL (the inline test cross-checks against the FK-equivalent Customer.address navigation; the split-rows test verifies one batched DataLoader round-trip across five films). Out of scope, follow-up filed: the legacy ReferenceElement { table, key, condition } directive surface conflates join-shape with WHERE-filter and admits seven free combinations; the cleanup is filed as a separate Backlog item (path-element-surface-cleanup, R235).

  • R229 (ebfa633 + b9121e0): EnumTypeGenerator now honours @field(name:) on enum values by writing .name(<sdl>).value(<runtime>) into the generated <Name>Type.type() body, with the runtime string pre-resolved at classify time on a new no.sikt.graphitron.rewrite.model.EnumValueSpec (sdlName, runtimeValue, description, deprecationReason, source) carried as List<EnumValueSpec> values on GraphitronType.EnumType. Pre-R229 the emitter echoed .name(SDL).value(SDL) and the directive lookup was re-evaluated independently at the resolver site, so a federated subgraph returning the runtime form (e.g. "FØDSELSNUMMER" for FODSELSNUMMER @field(name: ...)) hit Can't serialize value ... Unknown value 'FØDSELSNUMMER' at graphql-java’s Coercing layer. With the directive lifted into the .value(...) slot, graphql-java owns the wire ↔ runtime translation at the boundary in both directions, which collapses the Java-side CallSiteExtraction.TextMapLookup arm into Direct: the sealed permit, EnumMappingResolver.enrichArgExtractions, the TypeConditionsGenerator / TypeFetcherGenerator static *_MAP emit paths, the ArgCallEmitter / FieldBuilder switch arms, and the EnumMappingResolver field/constructor params on ServiceDirectiveResolver / TableMethodDirectiveResolver all retire. EnumMappingResolver.buildTextEnumMapping / .validateEnumFilter read from the classified model’s List<EnumValueSpec> (lookup via ctx.types) so the directive is read once at classify time and both consumers share the record component, eliminating the drift R263 reintroduced. Coverage: pipeline-tier GraphitronSchemaBuilderTest.EnumTypeCase.ENUM_WITH_FIELD_NAME_DIRECTIVE pins the classifier output (runtimeValue == "FØDSELSNUMMER"); existing PLAIN_ENUM / ENUM_WITH_DEPRECATED_VALUE cases reroute through EnumValueSpec; new execution-tier EnumSerializationExecutionTest pins three boundary scenarios (directive value round-trips through Coercing on output; input round-trip delivers the runtime form to the resolver; simple-value identity fallback); unit-tier EnumTypeGeneratorTest.typeMethod_routesFieldNameDirectiveIntoRuntimeValue pins the generated .name(sdl).value(runtime) shape; the R53 regression SERVICE_MUTATION_FIELD_NAME_OVERRIDE_TEXT_ENUM flips its assertion from TextMapLookup to Direct (same scenario, conversion has moved to the wire boundary). Out of scope (called out, filed as R231): graphitron currently lowers text-mapped-enum fields to GraphQL type String at emit time, so R229’s .value() lift is invisible to clients on Sakila’s textRating field; emitting those fields as the enum type is a separate structural fix. Self-review follow-up b9121e0 swept stale TextMapLookup / enrichArgExtractions javadoc citations across InputBeanResolver, ServiceCatalog, ServiceDirectiveResolver, TableMethodDirectiveResolver, ArgCallEmitter, BodyParam, ConditionFilter, InputColumnBinding, InputField, and MethodRef. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R230 (8f59529): Fix BodyParam.nonNull for nested input fields under a nullable enclosing arg. walkInputFieldConditions now ANDs an effectiveNonNull boolean (seeded from the InputTypeArg’s `nonNull() at projectFilters, narrowed at each NestingField recursion) into the value passed to every implicitBodyParam / compositeImplicitBodyParam callsite, so the emitter’s unguarded condition.and(...) branch only fires when every enclosing link is statically non-null. Pre-R230 a query like soknader(filter: HentSoknadInput): [Soknad!] with HentSoknadInput.soknadId: [ID!]! silently returned the empty set when filter was omitted, because the generator emitted condition.and(film.film_id.in(null)) and jOOQ renders .in(null) as the literal false. Producer contract pinned by a new body-param.nonnull-is-effective-runtime @LoadBearingClassifierCheck on walkInputFieldConditions (single annotation covers both producer sites in its description text ; audit requires producer-key uniqueness) paired with a @DependsOnClassifierCheck on TypeConditionsGenerator.buildConditionMethod. BodyParam.nonNull’s interface-level javadoc tightens to name the producer / emitter contract; the accessor’s one-liner reduces to a forward-pointer. Coverage: `NestedInputFieldEffectiveNonNullPipelineTest pins the three AND transitions on the classified slot (nullable arg → false, both non-null → true, nullable NestingField wrapper between non-null arg and non-null leaf → false); GraphQLQueryTest.filmsByEffectiveNullability_omittedFilter_returnsUnfilteredBaseline is the only tier that observes jOOQ’s .in(null) rendering, asserting the omitted-filter case returns the unfiltered baseline of 5 films rather than the empty set.

  • R223 (9c25edc + c8813a4): roadmap-tool gains a check-adoc-tables verify-phase subcommand that walks every authored .adoc under graphitron-rewrite/ and docs/, tracks five structural block types (|=== table, ---- listing, .... literal, //// comment, ++++ passthrough), and fails the build on any markdown-separator row (|---|---|, with optional GFM alignment colons) found outside all blocks. Asciidoctor renders markdown table syntax as paragraph text with literal pipes, so the typo was invisible until publish; the truth table at graphitron-rewrite/docs/argument-resolution.adoc § "Truth table (per input-field, per call site)" was carrying this shape and is converted to AsciiDoc [cols=...] + |=== syntax in the same commit. target/, node_modules/, and .git/ subtrees are skipped; .md files are out of scope (markdown table syntax is native there). AdocMarkdownTableCheckTest pins six fixtures: markdown separator outside any block is flagged; the same characters inside each of the five block types are not flagged; target/ directories and .md files are skipped by the walker; aligned (|:---|---:|) separators are flagged. CLAUDE.md "Writing style" gains a paragraph naming the rule and the new check. Out of scope (called out, owned by R227): mdBodyToAdoc does not translate markdown tables embedded in .md roadmap plans, so rendered roadmap .adoc under target/ still carries raw pipe rows; that render-side hole is tracked separately. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25; verify phase reports "no markdown-formatted tables in authored .adoc files".

  • R228 (de25b0a + ac5830d): GraphitronSchemaValidator now rejects inline TableField / LookupTableField whose @reference path carries a @condition step at build time, closing the gap that let those two variants reach a runtime UnsupportedOperationException while the four sibling variants surfaced a Rejection.Deferred build error. ChildField.TableField and ChildField.LookupTableField declare ConditionJoinReportable with their own EmitBlockReason values (TABLE_FIELD_CONDITION_JOIN_STEP, LOOKUP_TABLE_FIELD_CONDITION_JOIN_STEP) and displayLabel ("Inline TableField", "Inline LookupTableField"); SplitRowsMethodEmitter.unsupportedReason is the single predicate the validator and both inline emitters consult, so inline stubs render byte-for-byte the same message as before and the four four ChildField variants javadoc/comment sites widen to six. Tests: R58TypedRejectionPipelineTest gains inlineTableField_conditionJoinStep_rejectedAtBuildTime + inlineLookupTableField_conditionJoinStep_rejectedAtBuildTime pipeline-tier coverage; the existing seal-tracking assertion renames conditionJoinReportable_implementedByExpectedFourVariants → conditionJoinReportable_implementedByExpectedSixVariants; TableFieldValidationTest / LookupTableFieldValidationTest flip WITH_CONDITION_ONLY from "no error" to "stub surfaces as build error" and add LIST_WITH_CONDITION_ONLY. docs/manual/reference/diagnostics-glossary.adoc adds === table-field-condition-join-step and === lookup-table-field-condition-join-step paragraphs alongside the four existing entries (DiagnosticsDocCoverageTest gate). Sakila example: Category.similar was an inline-TableField ConditionJoin fixture deliberately admitted by the classifier and stubbed at runtime; the validator now rejects it at build time (intended outcome), so the field and its sole dependent CategoryConditions class are removed from the example along with the README.adoc reference. Out of scope (called out, owned by R3 item 5 + R129): lifting the condition-join restriction itself ; when item 5 ships all six variants' validator arms come out together with the two inline emitter stubs and the four runtime stubs in SplitRowsMethodEmitter. Note on test placement: spec named GraphitronSchemaBuilderTest but GraphitronSchema carries no rejections() accessor; fixtures landed in R58TypedRejectionPipelineTest (existing home for "build schema + run validator + assert typed rejection"). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R224 (5b202fb + d1e8fd6): LSP Diagnostics.validateFieldMember consults the field classification before falling back to the type-backing table, so @field(name:) on a @reference(path:) field validates the column against the path’s terminal table instead of the enclosing type’s @table. For ColumnReference / CompositeColumnReference arms the lookup now resolves through FieldClassification.tableName() (projected via CatalogBuilder.terminalTableName), mirroring the runtime’s ServiceCatalog.resolveColumnForReference walk; Column / CompositeColumn arms route the same way (equivalent target table, sourced from the classification); InputUnbound / Unclassified arms stay silent because the validator already emits a precise message; other arms fall through to the existing backing-driven dispatch. The fix also adds TypeContext.enclosingFieldOrInputValueDefinition so the dispatch resolves the SDL field name on input-side input_value_definition nodes too (the prior enclosingFieldDefinition walked only output-side field_definition). The new emitter site wears @DependsOnClassifierCheck(key = "field-classification-payload-faithful") against the existing CatalogBuilder producer. Tests: DiagnosticsTest gains three regression cases driving synthetic LspSchemaSnapshot.Built.Current snapshots: input @table + @reference retargets to terminal-table column, output @table + @reference mirror, and silence-on-Unclassified (no duplicate "Unknown column …​ on table '<enclosing>'" diagnostic). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R225 (9b753db): LSP Diagnostics.severityOf flips Rejection.Deferred from DiagnosticSeverity.Warning to Error, so the editor squiggle matches the mvn graphitron:dev finality (every Rejection variant throws ValidationFailedException, regardless of arm ; the actionable hint is the roadmap-item slug carried by the rejection, not the severity, reverting the R147 softening). ValidatorDiagnosticsTest.deferredMapsToWarningSeverity renamed to deferredMapsToErrorSeverity with its severity assertion flipped; RejectionSeverityCoverageTest unchanged (asserts only non-null). Build green: full graphitron-lsp test suite (347 tests) passes on Java 25.

  • R216 (2a15e15 + 70e41cb): LSP classification, hover, inferred-directive, completion, go-to-definition, and @field(name:) member-validation surfaces now walk extend type X { ... } declarations in parallel with type X { ... } definitions. A new closed-family DeclarationKind enum (graphitron-lsp/.../parsing/DeclarationKind.java, 12 constants spanning both *_type_definition and *_type_extension kinds) replaces the three out-of-sync Set<String> sources of truth in InlayHints, TypeContext, and DeclarationHovers; DeclarationKind.enclosing(Node) + DeclarationKind.walkAll(Node, Consumer) centralise the two walks every consumer used; isCarrier() filters the field-hover ancestor walk to coordinates where Parent.fieldName is meaningful. TypeContext.tableNameOf is rerouted through the classifier’s name-keyed projection on the snapshot (built.typeClassificationsByName().get(name) → tableNameFromClassification) so an extend type Customer { ... } whose @table-bearing definition lives in another file still resolves to the authoritative table name; the private tableNameOf(TypeClassification) helper in InlayHints lifts to TypeContext.tableNameFromClassification so inlay / hover / completion / definition / diagnostic surfaces share one switch. Snapshot threading reaches ReferenceCompletions.generate and Definitions.compute (both gain @DependsOnClassifierCheck(key = "type-classification-payload-faithful") so the audit test pins their new dependence on the type-classification projection); GraphitronTextDocumentService wires workspace.snapshot() to both. Tests: InlayHintsTest gains classification, inferred-@field, and absent-@table parity on extend type Query + extend type Customer; DeclarationHoversTest gains type-name and field-name hover parity inside extend type Customer; DiagnosticsTest gains @field(name:) member validation inside extend type Foo (unknown column + valid column); the existing ReferenceCompletionsTest.unknownTableReturnsEmptyForKey adapts to the snapshot-as-source-of-truth posture (classifier maps Foo to a missing table, completion empties). Self-review fix (70e41cb) dropped a defensive LinkedHashSet<Node> dedupe from walkAll ; tree-sitter ASTs are trees, the set guarded against an impossible scenario. Out of scope (called out): generator-side admission of extend type Foo @table(name:"x") { ... } (the classifier doesn’t see @table on extensions today, so the snapshot-routed tableNameOf stays silent on extension-declared @table without a corresponding definition ; lifting that constraint is a classifier-side change with its own roadmap item). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25; 1905 graphitron + 337 graphitron-lsp + sakila-example tests pass.

  • R217 (ee2802f + 72fd454): LSP inlay classification labels surface model leaf names, and a synthetic @table(name: "...") ghost now renders on declarations whose classification is Table / Node / TableInterface / TableInput but that carry no @table directive at all. LspClassificationLabels.projectionLabel / projectionTypeLabel return each projection record’s simpleName() verbatim ("Column", "Table", "DmlMutation", …​) via exhaustive uniform-body switches that survive as compile-time tripwires for new permits; DeclarationHovers prints the qualified form (**FieldClassification.Column** / **TypeClassification.Table**) in hover headers; FieldClassification / TypeClassification class-level Javadoc records the new dual role of projection-record names. The generator-permit fieldLabel(GraphitronField) / typeLabel(GraphitronType) variants had no LSP callers and are deleted. InferredDirectiveArgs.Entry gains an AbsentArm absentArm slot (initial implementation used a boolean renderWhenAbsent flag, replaced in the self-review fix by a sealed AbsentArm strategy interface so a future entry that wants absent-rendering must implement or reuse a permit: flipping the field on without a matching renderer no-ops at compile time, not at runtime, preserving the canonical-arg table’s invariant "downstream consumers either pick it up automatically or fail to compile"). Today only the @table entry carries an arm (AbsentArm.TableName) whose switch encodes the eligibility set and reads tableName() off TypeClassification.{Table, Node, TableInterface, TableInput}; @field / @reference stay off per the spec’s judgement calls. InlayHints.collectAbsentDirectiveHints walks type-definition nodes in parallel with the classification arm, dispatching to entry.absentArm().resolveAbsentValue(c) via virtual call, emitting @<directive>(<arg>: "<resolved>") anchored at the type-name node when the type carries no directive of that name; the existing present-but-bare arm and the new absent arm share the canonical-arg table and the config.inferredDirectives() toggle. The @DependsOnClassifierCheck(key="type-classification-payload-faithful") reliesOn text widens to note the absent-directive arm. Tests: InlayHintsTest.classificationHintsLabelFieldDeclarations flips from "table type", "column" to "Table", "Column"; new absentTableHintRendersOnObjectTypeWithoutDirective, absentTableHintRendersOnInputTypeWithoutDirective, and absentTableHintSuppressedWhenDirectivePresent pin the new arm on object + input declarations and assert it stays quiet when the directive node is present; inferredTableHintSuppressedWhenAuthored extends to noneMatch(label -> label.startsWith("@table")); DeclarationHoversTest flips header assertions to the qualified form. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25; 337 graphitron-lsp tests + 1897 catalog tests pass.

  • R215 (fdb757b + 3d60f40): column-binding requirement captured at classification, not derived at usage. BuildContext.classifyInputField gains a ClassifyContext parameter (carries expandingTypes + enclosingOverride); the recursive descent through NestingField composes ctx.expanding(typeName).withOverride(ctx.enclosingOverride() || nestOverride). InputField.ConditionOnlyField renamed to InputField.UnboundField(parentTypeName, name, location, typeName, nonNull, list, Optional<ArgConditionRef> condition, String attemptedColumnName); the classifier emits this variant uniformly on column-miss (across plain and @table inputs) and on @condition(override: true) with a matching column (the §5 ColumnField+override:true collapse). TypeBuilder.buildTableInputType defers column-coverage to consumption (admits UnboundField instead of rejecting the whole type as UnclassifiedType); FieldBuilder.walkInputFieldConditions becomes a single exhaustive switch with the UnboundField arm consuming enclosingOverride directly and emitting a consumer-side rejection (typed Rejection.AuthorError.UnknownName with Levenshtein hint) when the cascade doesn’t admit. GraphitronSchemaValidator walks TableInputType.inputFields() and rejects UnboundField + @condition(override: false) at the directive’s source location; MutationInputResolver.resolveInput admits UnboundField(condition: present, override: true) on UPDATE / DELETE and rejects on INSERT, plus rejects @condition(override: false) on any mutation input field at SDL-walk time. Eight downstream sealed-switch consumers updated (walkInputFieldConditions, MutationInputResolver, EnumMappingResolver, CatalogBuilder, ContextArgumentClassifier, GraphitronSchemaValidator, TypeFetcherGenerator.NOT_DISPATCHED_LEAVES, plus LSP LspClassificationLabels + DeclarationHovers and FieldClassification.InputUnbound renamed from InputCondition); InputFieldResolver.resolve(typeName, rt, enclosingOverride) takes the cascade flag from FieldBuilder.classifyArgument (fieldOverride || argCondition.map(c -> c.override()).orElse(false)). Two new load-bearing classifier-check keys: input-field.unbound-implies-no-column (producer: classifyInputFieldInternal; consumer: walkInputFieldConditions) and input-field.unbound-with-override-condition-admits-on-mutation-update-delete (producer: resolveInput). Coverage: eight new R215 acceptance tests in GraphitronSchemaBuilderTest (r215_plainInputArgLevelOverrideAdmitsNonBindingField, r215_tableInputNonBindingFieldRejectsAtConsumer, r215_tableInputNonBindingFieldAdmittedUnderOverrideCascade, r215_validatorRejectsOverrideFalseOnNonBindingField, r215_validatorRejectsConditionOverrideFalseOnMutationInputField, r215_mutationUpdateConditionOverrideTrueOnNonPkFieldAdmits, r215_mutationInsertConditionOverrideTrueRejects, r215_nestedPlainInputPropagatesCascade); R210’s renamed plainInput_overrideTrueWithoutMatchingColumn_classifiesAsUnboundField + tableInput_overrideTrueWithoutMatchingColumn_classifiesAsUnboundField stay green; six existing tests asserting the pre-R215 rejection shape (EXPLICIT_TABLE_UNRESOLVED_COLUMN, NESTED_INPUT_FIELD_UNKNOWN_COLUMN, NodeIdPipelineTest.InputCase.{ACCESSOR_MISSING, LIST_VARIANT}) updated to assert the new admit-at-type-build behaviour. Late-round patch (after self-review with principles-architect, alf’s pushback on cascade-contract gloss): the first-pass walkInputFieldConditions UnboundField arm silently dropped the inner @condition under an outer @condition(override:true) cascade, contradicting docs/manual/how-to/migrating-from-legacy.adoc#behavior-divergence-condition-cascade ("every @condition you write produces SQL; the override flag controls only the implicit column predicate"). The arm now mirrors the ColumnField arm structure (always emit the explicit @condition when present; decide rejection separately): rejects at the consumer outside the cascade for condition.isEmpty() (no filter contribution) and condition.isPresent() && !override() (structurally malformed shape) ; the second arm acts as a safety net for plain inputs until R221 lifts validator coverage there. New acceptance test #11 r215_innerExplicitConditionFiresOnUnboundFieldUnderOverrideCascade pins the cascade-doc contract (two ConditionFilter`s emitted: outer arg-level + inner field-level); the three R205 Path B regression tests (ArgumentParsingCase.PLAIN_INPUT_ARG_FIELD_CONDITION_EMITTED, `plainInput_unresolvedFieldWithCondition_rejectsAsUnclassifiedFieldWithUnknownName, plainInput_overrideFalseWithoutMatchingColumn_stillRejectsAsUnclassifiedField) keep their pre-R215 rejection assertions green via the consumer-arm safety net. Out of scope (called out, deferred to follow-ups): MutationField.{Value, Condition} sealed projection from MutationInputResolver for downstream DML emitters (acceptance behaviour shipped at the resolver’s per-field admission loop; the structural lift to a sealed projection is a follow-up roadmap item ; no emitter consumes a MutationField projection yet); R213 exact-SourceLocation attribution on the surrounding UnclassifiedField’s `location field (the rejection prose names the field but the wrapper’s location still points at the consuming query field; threading the location through walkInputFieldConditions → projectFilters → projectForFilter → TableFieldComponents.Rejected is the R213 follow-up); R221 validator walks PlainInputArg.fields() for the same UnboundField + @condition(override:false) rejection the shipped validator catches on TableInputType (the consumer-arm safety net above covers non-cascade plain inputs; the cascade case admits-and-emits today, which R221 will reject at the directive’s location). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25; 1906 graphitron tests + 334 graphitron-lsp tests + all sakila-example tests pass.

  • R211 (831a32d): @condition(override: true) build failure no longer surfaces the misleading "no column 'X' found in table 'Y'" line alongside the actionable condition error. BuildContext.classifyInputFieldInternal reinstates an errorsBefore size-delta check inside the R210 override:true block: when buildInputFieldCondition appends to errors and returns empty, the gate returns a placeholder InputFieldResolution.Unresolved(lookupColumn=null, "@condition(override: true) failed to build; see condition error") instead of falling through to the column-miss arm; the column is unused by construction under override:true. The override:false leg never enters this branch, so R205 acceptance test #6’s typed AuthorError.UnknownName lift survives (InputFieldResolver.resolve’s `canLiftToUnknownName guard sees condErrors non-empty + lookupColumn null and folds to Rejection.structural, which is the right bucket ; the failure shape is condition-method binding, not unknown-column). R210’s existing test plainInput_overrideTrueWithBrokenCondition_rejectsAsUnclassifiedField gains a doesNotContain("no column 'sakskode' found") assertion so a regression that reintroduces the column-miss arm under override:true trips at the existing test site. Surfaced by alf’s production opptak-subgraph (parameter-name mismatches in OpptakFilterInput.opptaksNavn / utdanningstilbud); the related attribution issue from the same investigation is filed as R213. Will be subsumed by R215’s column-binding-at-classification restructure (the override:true gate moves above the column lookup, making the column-miss arm structurally unreachable on this branch); the doesNotContain assertion stays as a regression guard. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R203 (2b8b465 + 8d39ef4 + 9ac3e4c Phase 1 natives module + release workflow; first release no.sikt:graphitron-tree-sitter-natives:0.26.0-1 published to Maven Central via the tree-sitter-natives-release workflow on its four-platform matrix [Phase 2]; ae486d9 Phase 3 graphitron-lsp cutover; bbabb02 BundledLibraryLookup probe for well-known libtree-sitter install prefixes; a94f546 In Review → Ready rework after first review pass; a3338b6 Phase 4 distinguishes too-old libtree-sitter from missing and ships the spec-named error-translation unit test): graphitron-lsp no longer compiles a per-platform tree-sitter runtime + grammar on every mvn install. The vendored libtree-sitter runtime sources and the bkegley tree-sitter-graphql grammar parser.c are gone from graphitron-lsp/src/main/native/ (≈30 000 lines deleted); the three build-native-* Maven profiles and the exec-maven-plugin shell-out are gone from graphitron-lsp/pom.xml. The grammar binary now ships from a new same-repo standalone Maven module graphitron-rewrite/graphitron-tree-sitter-natives/ (groupId no.sikt, version stream <tree-sitter-runtime-ABI>-<build-n>, first release 0.26.0-1; standalone pom intentionally not a child of graphitron-rewrite-parent and not in the parent reactor’s <modules> list, so the release cadence decouples from the rewrite’s 10-SNAPSHOT parent and mvn install -f graphitron-rewrite/pom.xml -Plocal-db pays zero build cost). The natives release workflow is workflow_dispatch-only on a four-platform GitHub Actions matrix (linux-x86_64, linux-aarch64, macos-aarch64, windows-x86_64; macos-x86_64 dropped during Phase 1 dry-run since Sikt LSP developers all run M1+); each matrix runner runs upstream’s tree-sitter build CLI against the vendored grammar to produce one platform-shaped shared library, the jar carries exactly four lib/<os>-<arch>/tree-sitter-graphql.{so,dylib,dll} entries (POSIX lib prefix; Windows unprefixed per platform convention), and a post-deploy load+parse matrix verifies the published artifact resolves into a fresh local m2 and the bundled grammar loads against an OS-installed libtree-sitter on every platform. graphitron-lsp BundledLibraryLookup switches to the four-platform set, drops the previous UnsupportedOperationException Windows branch, and now also probes well-known libtree-sitter install prefixes (Homebrew /opt/homebrew/lib + /usr/local/lib on macOS, vcpkg’s <VCPKG_ROOT|VCPKG_INSTALLATION_ROOT>/installed/x64-windows/bin + the default C:\vcpkg\... on Windows, /usr/local/lib on Linux), composing a system-installed runtime onto the SPI grammar lookup via SymbolLookup.or so vanilla brew install tree-sitter / vcpkg install tree-sitter:x64-windows work with no env-var wiring. GraphqlLanguage.loadOrExplain translates UnsatisfiedLinkError / RuntimeException from Language.load into an install-instructions message and now also distinguishes "too-old libtree-sitter installed" (commonly Debian/Ubuntu apt’s libtree-sitter0 0.20.x, which predates the ts_language_abi_version symbol jtreesitter 0.26 looks up) via a probe-path classifier that walks a BundledLibraryLookup-superset including apt’s /usr/lib/<arch>-linux-gnu/libtree-sitter.so.0 and checks the ABI symbol directly. GraphqlLanguageErrorTranslationTest pins the classifier (cause-chain walk, ABI-symbol failure shape, ignores unrelated errors), the missing- and too-old- runtime messages per OS via @EnabledOnOs, and explicitly names libtree-sitter0 in the too-old Linux hint. NativeLibraryBundleTest covers all four platforms via per-platform @EnabledOnOs methods (3 skipped on any single host); rewrite-build.yml source-builds libtree-sitter v0.26.9 so the linux-x86_64 method runs green in CI. getting-started.adoc gains a "Native runtime dependency" section with [#native-runtime-dependency] anchor and a per-platform install + library-discovery table (including a NixOS shell.nix snippet for nix-store layouts and the JAVA_TOOL_OPTIONS=-Djava.library.path=... escape hatch for non-default installs). Subsumes and deletes R89 (lsp-native-build-multiplatform-ci): the multi-platform-CI concern is now the post-deploy matrix on the natives release workflow plus the four @EnabledOnOs NativeLibraryBundleTest methods, not a per-PR matrix on rewrite-build.yml. 334 graphitron-lsp tests pass (7 skipped: 3 platform-gated NativeLibraryBundleTest + 4 platform-gated GraphqlLanguageErrorTranslationTest methods on the Linux CI host); full mvn -f graphitron-rewrite/pom.xml install -Plocal-db green on Java 25.

  • R210 (94bc3bf + 47f5f39 + ac2588c): @condition(override: true) on an input field with no matching column no longer rejects as Unresolved under R205’s Path B. New InputField.ConditionOnlyField sealed permit carries the ArgConditionRef with no column data; BuildContext.classifyInputFieldInternal gates on the directive’s override flag at the "no column found" fall-through (cheap read, no errors-list side effects) before building the condition, so the typed AuthorError.UnknownName lift on override:false reflection failures is preserved at the R205 boundary. FieldBuilder.walkInputFieldConditions emits the explicit ConditionFilter only; six other exhaustive InputField consumer sites (ContextArgumentClassifier, GraphitronSchemaValidator, EnumMappingResolver, CatalogBuilder + new FieldClassification.InputCondition record, TypeFetcherGenerator.NOT_DISPATCHED_LEAVES, LSP hover + inlay) grow explicit arms; MutationInputResolver’s existing default-arm rejects condition-only carriers as structurally unfit for DML. Symmetric across plain inputs (the reported opptak-subgraph `SakFilterV2Input.sakskode shape) and @table inputs since both share classifyInputFieldInternal. Coverage: three R210 acceptance tests (plainInput_overrideTrueWithoutMatchingColumn_classifiesAsConditionOnlyField with @ProjectionFor(ConditionOnlyField.class), tableInput_overrideTrueWithoutMatchingColumn_classifiesAsConditionOnlyField, plainInput_overrideTrueWithBrokenCondition_rejectsAsUnclassifiedField) plus a boundary test (plainInput_overrideFalseWithoutMatchingColumn_stillRejectsAsUnclassifiedField) pinning the R205↔R210 behaviour boundary by name; TestConditionStub gains sakskodeCondition / syntheticNameCondition fixtures; VariantCoverageTest.NO_CASE_REQUIRED carries the rationale for the @Test-not-enum-case shape. Out of scope (called out): execution-tier Sakila fixture mirroring the production shape ; deferred (pipeline tier already exercises classifyInputFieldInternal and the projected ConditionFilter). Design alternative considered: InputFieldResolution.ConditionOnly arm (resolution-tier sibling, structurally honest, deferred to a future refactor if the carrier-vs-resolution distinction becomes load-bearing); the carrier-tier InputField.ConditionOnlyField permit chosen for incremental change cost + uniform walking. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25; 1893 tests pass.

  • R205 (fdada52 restore plain-input filter symmetry + escalate Unresolved to build error, c1de3f8 self-review follow-ups + R209 backlog stub): plain-input (non-@table) filter input types now classify and project identically to @table inputs. InputFieldResolver.resolve returns sealed Resolution.{Ok, Rejected} (mirroring OrderByResolver.Resolved); any InputFieldResolution.Unresolved or @condition reflection failure lifts as a typed Rejection (single column-miss → Rejection.unknownColumn so LSP fix-its consume the structured attempt + candidates; everything else folds to Rejection.structural with joined prose). ArgumentRef.UnclassifiedArg’s `String reason becomes Rejection rejection with a backwards-compatible reason() accessor; the four other construction sites in FieldBuilder.classifyArgument wrap their prose with Rejection.structural(...). FieldBuilder.projectFilters’ `PlainInputArg branch is now structurally identical to TableInputArg: it allocates a non-null implicitBodyParams and drains it into bodyParams, so the symmetric implicit-predicate emission is a type-system fact (the four per-call implicitBodyParams != null guards in walkInputFieldConditions collapse; the method asserts non-null at entry via requireNonNull). projectFilters / projectForFilter thread List<Rejection> end-to-end via a new foldRejections helper; UnclassifiedArg.rejection.prefixedWith(...) preserves typed payloads (e.g. AuthorError.UnknownName from a plain-input column miss) through to UnclassifiedField.rejection. ProjectionCoverageTest drops the PojoInputType allowlist entry; a new @ProjectionFor(PojoInputType.class) projection test (plainInput_resolvedColumnWithoutCondition_emitsImplicitBodyParam) pins the implicit-predicate emission on the plain-input path. Six acceptance tests cover symmetric implicit emission, explicit+implicit composition, override propagation, rejection on Unresolved with/without @condition, and rejection on @condition reflection failure (Path B: bare-field-without-@condition signals binding intent just as much as @condition-annotated does). The languagesByPlainInput sakila-example fixture + execution test, which encoded the silent-drop as expected behaviour, are deleted; docs/argument-resolution.adoc retires the per-field-skip rationale paragraph at :400-412 and adds an R205 anchor sentence to the truth table at :262-275. Out of scope (called out): project-wide design-doc-vs-implementation conformance audit (filed as R207); auto-binding via @condition method when no column resolves; FieldRegistry classify-input trace’s typed-Rejection payload loss (filed as R209). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R206 (3a44d47): synthesised ConnectionType / EdgeType carry the @asConnection carrier field’s SourceLocation. ConnectionPromoter.promote now passes BuildContext.locationOf(fieldDef) into the two record constructors instead of null; first-write-wins on dedupe (the existing instanceof ConnectionType early-continue preserves the first carrier’s location) and PageInfoType.location() deliberately stays null because a single PageInfo serves every connection so no carrier site is the actionable one. Downstream, GraphitronSchemaBuilder.rejectCaseInsensitiveTypeCollisions already reads existing.location() when demoting to UnclassifiedType, so SYNTH_VS_SYNTH / SDL_VS_SYNTH / SYNTH_EDGE_VS_SDL ValidationError`s now carry an actionable position an LSP/editor can jump to. `CaseInsensitiveTypeClashCase migrated Consumer<GraphitronSchema> → BiConsumer<GraphitronSchema, String> so each arm sees its own SDL fixture; SYNTH_VS_SYNTH pins both line and column (via new TestSchemaHelper.preludeLineCount); SDL_VS_SYNTH and SYNTH_EDGE_VS_SDL pin the synth side’s carrier column; SYNTH_PAGE_INFO_VS_SDL explicitly asserts the synth member’s null location locking in the design choice. No record-shape changes; pure provenance threading.

  • R204 (930739a): validate uniform env.getSource() domain return type across OutputField producers on an SDL type. Lifts OutputField as a sealed sub-interface of GraphitronField (permits RootField, ChildField) declaring DomainReturnType domainReturnType(); new sealed DomainReturnType (Record(TableRef) | TableRecord(ClassName) | Plain(ClassName)) mirrors the producer’s env.getSource() Java domain identity without classloading at validator time. Post-classification GraphitronSchemaBuilder.validateUniformDomainReturnType groups OutputField entries by SDL Object return-type name and demotes every participant in a multi-arm group to UnclassifiedField with a typed Rejection.AuthorError.MultiProducerDomainTypeDisagreement; the validator carries @LoadBearingClassifierCheck(key = "output-fields.uniform-domain-return-type") paired with a matching @DependsOnClassifierCheck on FetcherEmitter.buildSingleRecordTableFetcherValue. The two formerly-@Disabled mixed-producer cases in SingleRecordTableFieldServiceProducerPipelineTest now assert against the unified-path diagnostic; unit-tier DomainReturnTypeCoverageTest walks the sealed-permit graph by reflection and pins per-arm structural equality. Per-permit narrowing (design fork from the spec draft’s broad-detection wording): table-bound service producers answer Record(table) rather than TableRecord(recordClass) because typed XRecord IS-A jOOQ Record and children read by name through the generic Record interface; only the carrier-payload case (DML @mutation Record(table) vs @service-on-Mutation TableRecord(XRecord) for the same payload SDL Object) surfaces today. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R190 (182ec24 sealed GraphitronContext + schema-driven Graphitron.newExecutionInput(DSLContext, …) factory: ContextArgumentClassifier walks every MethodRef.Param.Typed whose source is ParamSource.Context, rejects mutually-incompatible Java types per contextArgument name as a typed Rejection.AuthorError.TypeConflict, and stores one ResolvedContextArg(name, javaType, sites) per name on a new Classification carrier; GraphitronContextInterfaceGenerator now emits public sealed interface GraphitronContext with a nested public static final class GraphitronContextImpl implements GraphitronContext (same-compilation-unit permits, no javapoet permits surface needed) carrying a public INSTANCE field and a private constructor, getTenantId removed, getDslContext demoted to a default reading env.getGraphQlContext().get(DSLContext.class); GraphitronFacadeGenerator.newExecutionInput collapsed to a single overload with DSLContext defaultDsl first then one parameter per ResolvedContextArg in alphabetical order, body null-checks every slot and populates GraphQLContext with DSLContext.class, each contextArgument string key, and the singleton GraphitronContextImpl.INSTANCE under GraphitronContext.class; five DataLoader name emission sites de-prefixed (DataLoaderFetcherEmitter, TypeFetcherGenerator ×2, MultiTablePolymorphicEmitter ×2, QueryNodeFetcherClassGenerator); HandleMethodBody federation entity dispatch grouping collapses Map<Integer, Map<String, List<Object[]>>> to Map<Integer, List<Object[]>>; graphitron-sakila-example migrated end-to-end (deleted AppContext.java, GraphqlResource calls Graphitron.newExecutionInput(dsl, "test-user"), 14 anon-impl test sites collapsed, two getTenantId-override tests commented out with forward-reference to R45); 8d99486 14-page user-doc rewrite (getting-started.adoc, runtime-extension-points.adoc, runtime-api.adoc, test-your-schema.adoc substantive rewrites; tenant-scoping.adoc, apollo-federation.adoc, split-vs-inline.adoc deferral banners pointing at R45; index + in-prose touch-ups across how-it-works.adoc, batching-model.adoc, 06-going-further.adoc, add-custom-conditions.adoc, security.adoc, graphitron-rewrite/docs/README.adoc); b408253 L2 ContextArgumentTypeAgreementTest (accepted + three-site conflict fixtures) + L4 ContextArgumentTypeAgreementValidationTest (pins the validator-mirrors-classifier drain renders header + indented per-site lines and exposes the typed sites field); f1a6b7a L4 GraphitronFacadeGeneratorPipelineTest (classified two-@service(contextArguments)-site SDL; asserts alphabetical parameter ordering, per-slot requireNonNull, the graphQLContext lambda body’s typed/string puts, the DataLoaderRegistry attach), L5 example SDL gains the single @service(contextArguments: ["userId"]) site (Query.greetingByUser → UserGreetingService.greet), L6 FilmContextArgumentRoundTripTest (round-trip threading through to the service method; singleton-throws-on-missing-with-factory-hint diagnostic; hand-rolled-ExecutionInput.Builder-redacts-through-framework end-to-end); 0aa1ee7 self-review pass addressing principles-architect findings ; Classification cached on GraphitronSchema as a 6th component populated once at parse boundary (validator + facade emitter both read schema.contextArguments() rather than re-classifying, restoring the "single producer" framing the load-bearing-classifier annotations promised), dead graphitronContextCall parameter dropped from DataLoaderFetcherEmitter.build plus its three TypeFetcherGenerator call sites, CallParam lifted to carry TypeName javaType so ArgCallEmitter’s two Context arms read identical structural data instead of one round-tripping through `ClassName.bestGuess, body-string assertion on the pipeline test deleted (covered by L5 compile + L6 round-trip), Class<T> expectedType slot dropped from getContextArgument and the Java cast moved to the generated call site ((String) graphitronContext(env).getContextArgument(env, "userId")) ; the factory’s typed parameter list IS the load-bearing diagnostic and the runtime expectedType.cast was redundant ceremony; Classification.resolved switched from Map.copyOf to Collections.unmodifiableMap(new LinkedHashMap<>(…)) so the alphabetical TreeMap iteration order survives the defensive copy across JVM hash seeds): single-tenant slice of R45 lands the sealed contract + the schema-driven factory so the multi-tenant rescope can layer tenant-column classification, byTenant overload, per-loader name partitioning, and the @tenantId directive on top of a stable baseline. Out of scope (called out, all reserved for R45): tenant column Mojo config, tenant-scope classification, byTenant factory overload, DataLoader name partitioning by tenant, @tenantId ARGUMENT_DEFINITION directive. Custom validator factory (<validatorFactory> Mojo element) reserved for R192. Follow-ups flagged by self-review, non-blocking: the singleton’s INSTANCE is publicly callable (the L6 missing-value test reaches in directly to inspect the un-redacted message text; javadoc explicitly frames the throw as server-log surface only). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25; 1870 graphitron tests + 347 example tests pass.

  • R160 (c2dc8d1 C1-C3 sealed FieldClassification + TypeClassification projection families plus CatalogBuilder projector switches landing on LspSchemaSnapshot.Built.{Current,Previous} symmetrically, d02a0bb C4-C5 InlayHints provider with inferred-directive + classification arms plus InlayHintConfig and workspace/didChangeConfiguration push, 039841b C6 classification hover via sealed DeclarationHover parallel dispatch in Hovers.compute, 3d42eeb C7 docs/manual/reference/lsp-inlay-hints.adoc, f647ea5 self-review pass 1 adding the workspace/configuration initialisation pull, the InferredDirectiveArgs indirection, MutationService.tableName parity with QueryService, and a R160 leak from the user-facing doc, 366b07b self-review pass 2 co-locating projection-payload assertions inside GraphitronSchemaBuilderTest’s existing `// ===== <VariantName> ===== classifier blocks via @ProjectionFor plus the ProjectionCoverageTest drift-prevention meta-test): surfaces Graphitron’s inference and classification layers in the editor as inlay hints + rich hover. Three independent client-side toggles all default to false and live under graphitron.inlayHints.inferredDirectives (ghost annotations at bare @table / @field / @reference sites showing the resolved value), graphitron.inlayHints.classification (compact label per field declaration and type declaration), and graphitron.hover.classification (markdown unpacking the variant payload ; table, column, FK chain, target type, error channel, DML verb, …​). Both projection families are sized to distinct hover-payload shapes rather than 1:1 with the generator-side permits, with discriminator fields collapsing siblings that differ only in a label axis (e.g. four MutationField.DmlTableField permits collapse to one DmlMutation(tableName, inputTypeName, errorChannelName, DmlKind) record); the projector’s exhaustive switch over the generator-side permits is the load-bearing coverage contract that fails-to-compile on a new leaf without an LSP-side projection arm. Inferred-directive provenance is read from the live tree-sitter Tree on WorkspaceFile.tree() at request time (the AST asks "did the buffer carry name:?") rather than lifted onto the model ; Provenance discriminators were attempted under a previous design and rolled back (~330 lines across TableRef, ColumnRef, the five @reference-permits, ParticipantRef.CrossTableField); the AST-read keeps the parse boundary closed and adds no model surface. BuildArtifacts shape unchanged: the new fieldClassificationsByCoord / typeClassificationsByName fields live inside LspSchemaSnapshot.Built.{Current,Previous} symmetrically, Workspace.demoteSnapshot preserves them, and stale-snapshot rendering mirrors userArgHover / columnHover’s "prefer stale info over silence" policy. C6 introduces a sealed `DeclarationHover family (FieldDeclarationHover / TypeDeclarationHover) parallel to the directive-arg-keyed Behavior family rather than widening Behavior (the "Capability vs. sealed-switch confusion" principle: Behavior stays directive-argument-binding-shaped, SDL declaration coordinates get their own resolver). Two new @LoadBearingClassifierCheck keys (field-classification-payload-faithful, type-classification-payload-faithful) wear on CatalogBuilder.buildSnapshot with matching @DependsOnClassifierCheck annotations on the three LSP consumers (inferred-directive arm, classification arm, classification hover). Tests: pipeline-tier GraphitronSchemaBuilderTest blocks gain @ProjectionFor-annotated sibling assertions running each canonical fixture through the projector and pinning the projected record type + payload values (24 new @Test methods over ~20 variant blocks); ProjectionCoverageTest walks GraphitronField / GraphitronType sealed leaves and fails on any leaf without a @ProjectionFor cover or a documented NO_PROJECTION_REQUIRED exception; LSP-tier InlayHintsTest covers config gating, the three inferred-directive arms, the classification arm, and Built.Previous stale rendering; DeclarationHoversTest covers field-name and type-name cursor positions, the DmlMutation payload shape, the directive-arg cursor short-circuit, and Unavailable / missing-projection no-ops; GraphitronLanguageServerTest covers the initialisation-time workspace/configuration pull; GraphitronWorkspaceServiceTest covers the push-side didChangeConfiguration parse. Out of scope (called out, not regressed): inlay hints for inferred arguments on directives other than @table / @field / @reference (@nodeId(typeName:), @reference(key:) inference, and future cases extend InferredDirectiveArgs.ENTRIES and gain a renderer arm); inferred return shapes on root fetchers and the inferred join key on @nestingField; a graphitron-shipped editor extension (the LSP exposes the config keys; the editor flips them); inlay hints reflecting unsaved buffer state without a successful generator pass (hints derive from the snapshot, Unavailable means no hints). Follow-ups flagged by review, non-blocking: LspClassificationLabels.{fieldLabel,typeLabel}(model permit) switches are unused at runtime (projection-keyed callers cover both consumers) and can be deleted in a cleanup pass; InferredDirectiveArgs lives in the catalog package with its own string literals rather than under BuildContext’s `ARG_NAME / ARG_PATH, so a rename or new inference rule still maintains two places ; consolidate when a third consumer surfaces. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25; 1869 graphitron tests + 318 graphitron-lsp tests pass.

  • R191 (2e84eff initial-import omnibus carrying spec + implementation + tests): honor @field(name:) for accessor lookup on free-form @record parents on the table-bound and polymorphic-hub branches. FieldBuilder.collectAccessorMatches takes an accessorBaseName parameter and matches against <base> / get<UcBase> / is<UcBase> instead of the SDL field name; deriveAccessorRecordParentSource, resolveRecordParentSource, and derivePolymorphicHubSource thread the value from their callers (fieldName retained for cardinality-mismatch text that quotes the SDL name). The TableBoundReturnType arm at FieldBuilder.java:3700 reuses the already-computed columnName; classifyRecordParentPolymorphicChild reads @field(name:) at :4327-4329 before dispatching to resolvePolymorphicRecordParent. The accessor-rowkey-shape-resolved and accessor-rowkey-shape-resolved-against-hub @LoadBearingClassifierCheck description blocks each gain a sentence: matched accessor’s name is the directive value when present on a free-form @record parent, else the GraphQL field name. The AccessorRef.methodName() value remains the actual reflected method name, so emitters (buildAccessorKeySingle / buildAccessorKeyMany, TypeFetcherGenerator.buildRecordBasedDataFetcher) invoke by name without caring how it was selected. Restores symmetry with the scalar/result branch on the same parent shape (resolveRecordAccessor already threaded the directive value as accessorBaseName). Coverage: pipeline-tier GraphitronSchemaBuilderTest.AccessorDerivedSourceCase adds ACCESSOR_ROWKEYED_FIELD_NAME_REMAPS_ACCESSOR (admit on RemappedPayload with @field(name: "filmRecord"), asserts RecordTableField + AccessorCall.accessor().methodName() == "filmRecord" + cardinality ONE) and ACCESSOR_ROWKEYED_FIELD_NAME_REJECTS_WITHOUT_DIRECTIVE (pin the divergent-accessor-no-directive arm still falls through to the three-option AUTHOR_ERROR); RecordParentMultiTablePolymorphicPipelineTest.childInterfaceField_recordParent_accessorKeyedMany_fieldNameRemapsAccessor covers the polymorphic-hub admit (ListPayload parent, SDL field referrers with @field(name: "films"), asserts hub film + AccessorCall.methodName == "films" + cardinality MANY). New AccessorPayloads.RemappedPayload(FilmRecord filmRecord) fixture record. Out of scope (called out): renaming or restructuring @field(name:), the FK-derivation path (catalog-metadata-driven, structurally indifferent to the directive), and the three-option rejection text. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db -P!docs on Java 25.

  • R194 (5e5f5e3 builder pass + 5 pipeline cases, a1feace self-review: EmitsPerTypeFile capability + typed CaseFoldCollision + 2 cases, f69b479 preserve CaseFoldCollision under prefixedWith): rejects case-insensitive type-name collisions at build time. GraphitronSchemaBuilder.rejectCaseInsensitiveTypeCollisions runs post-ConnectionPromoter.rebuildAssembledForConnections (rather than post-promote, so the assembled GraphQLSchema typeRefs stay resolvable when a synth Connection is demoted) and case-folds via Locale.ROOT; every member of each case-equivalent group demotes to UnclassifiedType carrying a typed Rejection.InvalidSchema.CaseFoldCollision(group, origin, prefix) with Origin ∈ {SDL, SYNTH_CONNECTION, SYNTH_EDGE, SYNTH_PAGE_INFO}. message() specialises the actionable fix hint per origin (@asConnection(connectionName:) for synth arms, generic rename for SDL); validateUnclassifiedType projects one ValidationError per member. The emit-vs-no-emit split is lifted out of GraphitronType onto a new EmitsPerTypeFile capability marker (mirrors SqlGeneratingField / BatchKeyField), implemented by every variant except ScalarType and UnclassifiedType; detector filters via instanceof EmitsPerTypeFile. prefixedWith returns a same-variant CaseFoldCollision with accumulated prefix rather than degrading to Structural, satisfying R58’s typed-rejection-preserved-under-wrap contract (the validator’s prefixedWith("Type 'X': ") is the only path that reaches ValidationError.rejection). Coverage: GraphitronSchemaBuilderTest.CaseInsensitiveTypeClashCase parameterised over 7 SDL fixtures (SDL_VS_SDL, SYNTH_VS_SYNTH, SDL_VS_SYNTH, SYNTH_EDGE_VS_SDL, SYNTH_PAGE_INFO_VS_SDL, THREE_WAY_GROUP, NO_CLASH_BASELINE); RejectionRenderingTest.prefixedWithPreservesCaseFoldCollisionTypedFields pins single + re-prefixed paths; RejectionSeverityCoverageTest + SealedHierarchyDocCoverageTest pick up the new permit; typed-rejection.adoc carries the prose + mermaid-class entry. Out of scope (called out): legacy MakeConnections / graphitron-schema-transform classifier, auto-mangling colliding names, federation cross-subgraph clashes, derived-filename collisions beyond the type-name stem. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R196 (c42ed64 route Workspace recalc through listener seam): lifts "drain follows enqueue" from author-discipline at three of six Workspace public mutators (didOpen, didChange, didClose paired with explicit publishDiagnosticsForRecalculate(); setBuildOutput, demoteSnapshot, markAllForRecalculation unpaired and reachable from DevMojo.regenerate / rebuildCatalog after schema-file + classpath watcher events) to a structural invariant across all six. New private enqueueAndNotify(Runnable) helper performs the queue mutation under lock and fires a single-slot volatile Runnable recalculateListener after lock release; the six public mutators route through it (setBuildOutput / demoteSnapshot transitively via markAllForRecalculation), so toRecalculate writes only happen inside the funnel. GraphitronTextDocumentService.setClient registers this::publishDiagnosticsForRecalculate as the listener; the explicit publish calls in didOpen/didChange/didClose are gone, and the build-trigger paths now publish diagnostics on save without waiting for the next keystroke. didClose’s "clear-for-closed-file" one-shot stays, repositioned before the workspace call to keep the seam uniform (the only direct client call in the LSP service is the close-clear; everything else flows through the listener). Lock-release-before-listener-fire is deliberate: it keeps a build swap on the watcher thread and an editor event on the lsp4j thread from serialising on `lock through the heavy Diagnostics.compute body; idempotency on the drain side (a second drainRecalculate after the first empties the queue returns an empty list) makes "listener fires twice for two mutations interleaved with one drain" a no-op rather than a hazard. Tests: unit-tier WorkspaceTest.everyPublicQueueMutatingMethodFiresTheListener parametrises over the six mutators asserting listener-fire count delta of exactly 1; recalculateListenerDefaultsToNoOpForTestHarnesses pins that mutators on a workspace without setRecalculateListener do not NPE; drainRecalculateIsIdempotentOnEmptyQueue pins the single-extraction property the listener path depends on; demoteSnapshotOnNoOpDoesNotFireListener parametrises over the two no-op starting states (Unavailable, Built.Previous) pinning the only public-mutator path that returns without firing the listener ; the exception branch of the otherwise-uniform "every public mutator notifies" rule. Pipeline-tier BuildTriggerPublishesDiagnosticsTest captures publishDiagnostics calls on a stub LanguageClient and drives the three-step sequence (didOpen empty → setBuildOutput with validator error → setBuildOutput with empty report); pre-R196 the second assertion failed because the listener didn’t fire. Retires the LSP-side half of R149’s deferred end-to-end publish-diagnostics wire test (the producer-side buildOutput() report-population test stays under R149). Out of scope (called out, not regressed): multi-consumer fan-out (one consumer today; lift when a second appears); richer event shapes (sealed RecalculateEvent discriminating editor / build / demotion causes ; drain is cause-agnostic, sub-taxonomy carries no information consumers act on differently); non-DevMojo callers of markAllForRecalculation/demoteSnapshot/setBuildOutput. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R197 (a755e39 wire LSP didSave to in-process regen trigger, 30473f9 In Progress → In Review): wires GraphitronTextDocumentService.didSave to a Consumer<String> onSchemaSaved listener constructor-injected via GraphitronLanguageServer and propagated through DevServer from DevMojo. DevMojo.buildSaveListener(suffixes, debounce, regen) (package-private static) filters URIs by RewriteContext.schemaFileExtensions() and schedules regenerate(workspace) through the same schemaDebounce the FS watcher uses, so editor saves and watcher events coalesce on a single regen. DebounceExecutor construction hoisted from startSchemaWatcher up into execute() so the listener can be built before bindServer. Headless LSP-only use sites (standalone Launcher, existing TextDocumentServiceTest fixtures) keep their behaviour via no-arg / one-arg constructor defaults that pass a no-op Consumer. Workspace is unchanged: the seam lives at the language-server boundary, not in the workspace, so extension-set ownership stays in the Mojo and the LSP module remains suffix-agnostic. Tests: TextDocumentServiceTest.didSave_invokesListenerWithUri pins the URI-typed listener contract, didSave_noopWhenListenerAbsent pins the headless contract, DevMojoTest.saveListener_schemaSuffixSchedulesRegen covers the suffix filter and debounce scheduling. Docs: getting-started.adoc dev-loop prose calls out the dual-path model (LSP didSave primary, FS watcher headless fallback) and the Mermaid diagram gains the LSP didSave -> dispatch arrow; DevMojo class-level javadoc updated to match. Out of scope (called out, not regressed): didChange regen (save is the user’s intentional commit point; mid-typing buffers are partial SDL); replacing SchemaWatcher (stays as headless fallback and remains the only path for classpath watching); FSEvents native backend (deferred under R198’s out-of-scope list). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R198 (b39d1c5 lift SchemaWatcher FS-bound tests to synthetic dispatch, e10785c self-review: package-private dispatch + run()-time polling hint): lifts the five SchemaWatcherTest cases that depended on real-FS event delivery (modifyingGraphqlsFile_firesCallback, deletingGraphqlsFile_firesCallback, rapidWrites_firesCallbackOnce, newSubdirectory_isRegisteredAndFiresCallback, and one CatalogRefreshTest case) onto synthetic WatchEvent values driven directly into SchemaWatcher.dispatch. macOS’s JDK ships PollingWatchService with a hardcoded 10 s period (since SensitivityWatchEventModifier was removed in JDK 21), so the suite’s 1.6 s wait could never observe a real-FS event; the assertions the failing tests made were unit-tier invariants on dispatch (suffix filter, OVERFLOW reschedule, on-the-fly subdirectory registration) dressed up as integration tests of the JDK’s WatchService. writingGraphqlsFile_firesCallback survives as the Linux-only inotify smoke (@EnabledOnOs(LINUX)); nonGraphqlsFile_noCallback deleted as a duplicate of dispatch_ignoresUnconfiguredSuffix; graphqlsWriteDoesNotFireClasspathWatcher folded into the synthetic shape. New watchServiceBackend_matchesExpectedPerOs probe pins PollingWatchService on macOS and LinuxWatchService on Linux, so a future JDK shipping an FSEvents-backed WatchService would fail loudly and the Linux-only gate would get revisited. Runtime hint on the first iteration of SchemaWatcher.run() emits two LOGGER.info lines when the underlying WatchService is polling-based (JDK fact + LSP recommendation as separate lines so either can be revised independently); fires once per watcher lifetime in production, silent in synthetic-dispatch tests. SchemaWatcher.dispatch stays package-private; the lone cross-package consumer (CatalogRefreshTest in ..maven.dev) routes through a new test-only DispatchTestSupport class under src/test/java/.../maven/watch/, matching the existing watchedDirs() test-seam precedent. Test counts: SchemaWatcherTest 11→11, CatalogRefreshTest 2→2. Out of scope (called out, not regressed): swapping the WatchService backend to a native FSEvents library (would add JNA to the plugin’s classpath; size separately); the LSP-driven regen path itself (R197); Linux aarch64 / Windows verification (R89). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R189 (666f0fb admit FK-target @nodeId input fields on @mutation, 9524885 In Progress → In Review): admit InputField.ColumnReferenceField / CompositeColumnReferenceField (FK-target @nodeId(typeName: T) pointing at another @table’s NodeType, classified to `Resolved.FkTarget.DirectFk) on every non-UPSERT @mutation verb. The reference carriers' liftedSourceColumns live on the input’s own table, the extraction is narrowed to CallSiteExtraction.NodeIdDecodeKeys, and the emitters bind decoded keys against liftedSourceColumns positionally ; the same shape the same-table ColumnField / CompositeColumnField NodeId carriers already drive. MutationInputResolver.resolveInput drops the deferred R24 rejection for these two carriers on INSERT / UPDATE / DELETE and removes the misleading "tracked in R24’s scope" hand-off; UPSERT stays refused at the kind gate (R145). InputField.LookupKeyField and InputField.SetField widen permits to include both reference carriers. Load-bearing: EnumMappingResolver.buildLookupBindings adds case ColumnReferenceField / case CompositeColumnReferenceField arms emitting MapGroup / DecodedRecordGroup over liftedSourceColumns(); without this the mutation-input.where-columns-cover-pk check would silently under-count reference contributions and fire false "missing PK column" rejections on schemas whose FK column covers the PK. TypeFetcherGenerator extracts five new helpers (emitSetMapPuts, emitSetExcludedPuts, emitSetVColNameAdds, emitSetBulkCellAdds, emitSetVFieldPuts plus setFieldColumns / setFieldNodeIdExtraction dispatchers) that replace eight (InputField.ColumnField) sf casts over tia.setFields() across the UPDATE / UPSERT-SET / bulk-UPDATE paths; the INSERT-path helpers (anyNodeIdCarrier, buildInsertColumnList, buildPerCellValueList, buildInsertDecodeLocals) widen their carrier-shape switches with mirroring reference arms. Three @LoadBearingClassifierCheck annotations are restated: mutation-input.where-columns-cover-pk (semantic ; filter-column contributions now include liftedSourceColumns() from the two reference carriers), mutation-input.update-set-fields-equal-value-marked (wording ; admissible-carrier set widened), mutation-input.lookup-binding-decoded-record-arity-matches-carrier-columns (wording ; arity guarantee extended to the FK-target composite arm). Tests: pipeline-tier MutationDmlNodeIdClassificationTest adds eight R189 cases ; arity-1 INSERT admission, arity-1 DELETE PK-coverage, arity-1 UPDATE with @value SET field, composite-key DELETE through reordered_pk_parent / reordered_fk_child, composite-key INSERT, and three bracketing rejection cases: fkTargetNodeIdRef_pkCoverage_underCount_negativeRejectionFixture (the load-bearing assertion that pins the validator widening ; without step 4 this exact shape would fire a false "missing: id_1" rejection that slips past both compilation and execution tiers), fkTargetNodeIdRef_pkCoverage_genuinelyMissing_rejected (contrast fixture confirming the canonical missing-PK rejection still fires), and fkTargetNodeIdRef_upsert_stillRejected_underR144 (UPSERT refusal at the kind gate supersedes admission). Out of scope (called out, not regressed): Resolved.FkTarget.TranslatedFk admission (the parent_node + child_ref shape where the FK targets a non-PK NodeType keyColumn ; BuildContext.java:1846-1849 continues to produce InputFieldResolution.Unresolved); UPSERT (R145 territory); the output-side JOIN-with-projection NodeId encoding R24 originally hand-off pointed at. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R187 (8e70b8c In Review: nested @service arg-mismatch diagnostic at non-SOURCES shapes): the unresolved-@service-parameter discriminator in ServiceCatalog now selects the arg-mismatch arm whenever the parameter type is not SOURCES-adjacent, instead of gating that arm on parentPkColumns.isEmpty(). Nested fields with a non-container parameter (LocalDate, String, Integer, …​) whose name doesn’t match any GraphQL argument no longer fall through to "unrecognized sources type" ; they get the actionable "does not match any GraphQL argument or context key" hint with available args + argMapping suggestion. The DTO-shape rejection arm is gated to nested coordinates only (!parentPkColumns.isEmpty()), preserving the root + List<DTO> → arg-mismatch precedence pinned by dtoSources_onRootField_pointsAtArgCtxMismatch (this is the Spec’s "Precedence between DTO-hint and arg-mismatch" rule; the implementation deviates from Spec step-3’s literal wording, which would have moved the DTO arm up unconditionally and broken that test, but matches the Spec’s stated intent). Unit-tier ServiceCatalogTest rewrites reflectServiceMethod_unrecognisedParam_onChildField_* (now asserts arg-mismatch on getWithUnknown(Object) under non-empty parentPkColumns) and adds reflectServiceMethod_nonSourcesPayloadOnChildField_pointsAtArgCtxMismatch pinning the LocalDate reproduction. Pipeline-tier GraphitronSchemaBuilderTest adds SERVICE_ON_CHILD_WITH_NON_SOURCES_PARAM_NAME_MISMATCH_REJECTED alongside the existing root-coordinate rejection case. Follow-up R193 (Backlog) captures the architectural smell flagged by the principles-architect review: the discriminator now has two consumers (R185 + R187) with subtly different precedence; a sealed UnresolvedParam classifier would consolidate the precedence in one place.

  • R185 (b6539b9): narrow ServiceCatalog.looksLikeSourcesShape to List<RowN> / List<RecordN> only. A root @service whose Java parameter is a List<XRecord> (a concrete TableRecord subtype) under a name that doesn’t match any GraphQL argument was getting the "`@service` at the root does not support List<Row>/List<Record>/List<Object> batch parameters" diagnostic, shadowing the actionable arg-mismatch diagnostic that lists available argument names and suggests argMapping. List<XRecord> at root is the canonical InputBeanResolver shape, so a plain name typo collided with the Sources-shape exception. The concrete TableRecord branch is removed from looksLikeSourcesShape; only the two anonymous-key shapes (RowN, RecordN) keep producing the Sources-batch diagnostic. The user-visible diagnostic also drops /List<Object> to match what now triggers it. Pipeline-tier SERVICE_AT_ROOT_WITH_TABLERECORD_PARAM_NAME_MISMATCH_REJECTED (GraphitronSchemaBuilderTest) asserts the arg-mismatch diagnostic wins for List<FilmRecord> under a mismatched name; the two existing RowN-element cases (SERVICE_AT_ROOT_WITH_SOURCES_PARAM_REJECTED, MUTATION_SERVICE_WITH_SOURCES_PARAM_REJECTED) keep passing and lock the predicate against opposite regression.

  • R183 (3ccd1ea GitLab pipeline targets graphitron-rewrite reactor on tags, bf5d2c3 provision postgres service + run codegen + tests in publish, 40790fc apt-get gcc so graphitron-lsp’s native build works, 236860f parameterize test.db.url so CLI -D reaches surefire, dcdf0d2 self-review cleanups before re-handoff): replaces the legacy reactor’s snapshot-on-default-branch + release-on-tag publish pipeline with a release-only pipeline targeting the rewrite reactor. publish:snapshot is deleted outright; default-branch pushes (including GitHub → GitLab mirror sync) no longer fire any deploy, eliminating the active hazard of legacy 9-gitlab-SNAPSHOT artifacts accumulating in the Sikt GitLab Packages registry on every mirrored commit. publish:release now runs mvn -f graphitron-rewrite/pom.xml versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false -DprocessAllModules=true (the processAllModules flag is load-bearing: without it the aggregator pom updates but child modules keep ${revision} and the deploy publishes mismatched coordinates) followed by mvn -f graphitron-rewrite/pom.xml clean deploy -P gitlab,local-db -Ddb.url=jdbc:postgresql://postgres:5432/rewrite_test -Dtest.db.url=jdbc:postgresql://postgres:5432/rewrite_test. The tag regex widens to ^v\d+\.\d+\.\d+(-RC\d+)?$ to accept the -RC<n> suffix Maven Central consumers depend on. A new gitlab profile in graphitron-rewrite/pom.xml declares the GitLab Packages <repository> (no <snapshotRepository> ; the rewrite parent’s invariant per graphitron-rewrite/docs/README.adoc Publishing: an accidental mvn deploy on 10-SNAPSHOT must fail fast) plus deployAtEnd=true and the sources-jar attachment; the root-pom legacy gitlab profile is unreachable from the new caller and dies with the legacy reactor under R182. The pipeline image bumps from maven:3.9-eclipse-temurin-21 to maven:3.9-eclipse-temurin-25 to satisfy the parent pom’s requireJavaVersion enforcer rule. In-runner Postgres: publish:release provisions a postgres:18-alpine service (alias postgres, POSTGRES_HOST_AUTH_METHOD=trust), apt-get install`s `postgresql-client + gcc in the runner, and applies graphitron-rewrite/graphitron-sakila-db/src/main/resources/init.sql via psql. The local-db profile is activated alongside gitlab so jOOQ codegen reads against the live service rather than spinning up a Testcontainer (no Docker-in-Docker). The gcc install covers graphitron-lsp’s `build-native.sh invocation at generate-resources that compiles the tree-sitter native lib bundled into the published graphitron-lsp jar at lib/linux-x86_64/libtree-sitter-graphql.so. Surefire parameterisation: graphitron-sakila-example’s `local-db profile lifts test.db.url / test.db.username / test.db.password from literal <systemPropertyVariables> entries into pom <properties> (localhost defaults unchanged for local devs) so CLI -Dtest.db.url=... overrides reach surefire’s fork via property interpolation; without this, the CI’s overrides reached jOOQ codegen but the tests still tried localhost. Tests run in the publish pipeline as a deploy-boundary sanity check now that a real Postgres is in the runner anyway; the earlier "skip tests, GitHub gates them" rationale was load-bearing only when Docker-in-Docker was the alternative, and the GitHub publish workflow on main is itself in flux per R182 so cannot be relied on as a gate. Out of scope (called out, not regressed): retiring the legacy reactor or the root-pom gitlab profile (R182 collapses both); Maven Central publishing on main (separate fix once R182 retires the legacy reactor; this item only touches GitLab); cleanup of existing junk 9-gitlab-SNAPSHOT artifacts already deposited (manual GitLab UI task); a release-cli job creating a GitLab Release object attached to the tag (cosmetic ; consumers depend by Maven coordinate). Verification deferred to a throwaway-tag push (e.g. v10.0.0-RC0) confirming the five expected coordinates deploy to GitLab Packages (no.sikt:graphitron-rewrite-parent pom, no.sikt:graphitron-javapoet, no.sikt:graphitron, no.sikt:graphitron-maven-plugin, no.sikt:graphitron-lsp) while the six maven.deploy.skip=true modules stay out of the registry, and a default-branch push triggers no publish job. Build green locally: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25 with the local-db profile pointing at a native Postgres.

  • R178 (9ee35d9 step 1 DML-only cutover, e41ddb4 step 2a wire transportForParent through selectErrorsTransport, 64dfa33 step 2b @service-carrier classifier wiring, fe270f9 step 3 retire SettKvotesporsmal-bug carrier-walk consultations, c44193b Phase 4 slice 1 delete dead writers, 7cbe4b6 slice 2 retire transitional consultations, 1211e6d slice 3 non-DELETE structural detection, e665e11 slice 4 DELETE arm structural detection, 1e71906 slice 5 lift structural carrier scan to BuildContext, d0de975 slice 6 retire verbless walk’s carrier-walk consultation, 475e2ec slice 7a retire carrier-walk methods + load-bearing re-anchors, a1bbbcd slice 7b retire sealed model types, e06f1c8 slice 7c retire SingleRecordIdentityField permit, 83d2182 slice 7d cosmetic, 4a67b26 self-review cleanup, 1ddb22b rename "Carrier" identifiers to "Payload", f42b819 final self-review, 575cd9b In Progress → In Review): collapses the parallel single-record carrier walk to the unified SourceKey + R96 reflection path. Deletes seven sealed hierarchies (SingleRecordCarrierResolution, SingleRecordCarrierShape, CarrierFieldRole, DataElement, BuildContext.tryResolveSingleRecordCarrier overloads + classifyCarrierField, BuildContext.carrierProducerRegistry, the four register*CarrierDataField writers, ChildField.SingleRecordIdentityField) and three load-bearing keys (single-record-carrier-shape.roles-exhaustively-classified, carrier-data-field.single-producer-kind, carrier-data-field.service-producer-strict-return). Adds two sibling ProducerBinding arms (DmlEmitted(TableRef, DmlKind, Cardinality), ServiceEmitted(TableRef, Cardinality, producer-site)) grounded by R96 in dedicated memos, observed structurally on the payload SDL so the carrier walk’s forbidden-directives loop never fires at observation time (the SettKvotesporsmal bug’s mechanism). A builder-internal sealed result BuildContext.DmlPayloadScan (Admit(dataField, DmlElementKind) | Reject(reason) | NotApplicable) replaces the carrier walk for the @mutation classifier and MutationInputResolver. The @field directive on a non-$source payload data field no longer hard-rejects ; the SettKvotesporsmal contract pin: with and without @field(name:), semantically identical schemas classify identically. The mutation-dml-record-field.data-table-equals-input-table invariant re-anchors on the smaller requireDmlDataTableMatchesInputTable helper covering both DELETE and non-DELETE arms; the error-channel.local-context-transport invariant re-anchors on FieldBuilder.detectStructuralDmlErrorChannel (the new sole producer of ErrorChannel.LocalContext on DML payloads). The unit-tier ErrorsTransportSelectionTest pins the errors-field defaulting rule table; the pipeline-tier SettKvotesporsmalShapeRegressionTest pins both the identical-classification and the diagnostic-wording contracts. The three payload-returning mutation permits (MutationDmlRecordField, MutationBulkDmlRecordField, MutationServiceRecordField) survive structurally; what changes is their classification path. Tests: unit-tier ErrorsTransportSelectionTest (8 cases pinning every branch of FieldBuilder.selectErrorsTransport); pipeline-tier SettKvotesporsmalShapeRegressionTest (3 cases: with-@field admits, without-@field admits identically, ClassBacked return-mismatch diagnostic cites the payload class not the inner record); pipeline-tier SingleRecordPayloadPipelineTest (renamed from SingleRecordCarrierPipelineTest, 33 cases including R178 admits of @field and @deprecated on the data field); execution-tier SingleRecordPayloadDmlTest (renamed, durability pins intact); the MUTATION_DML_RECORD_FIELD / MUTATION_BULK_DML_RECORD_FIELD / DELETE-carrier / SINGLE_RECORD_IDENTITY_FIELD_ORPHAN rows in GraphitronSchemaBuilderTest retarget to the unified path. Phase 5 deferral (called out in spec, separable follow-up): the emit-side migration to Wrap.Row + Reader.ColumnRead ships under R180 (record-parent-column-read-helper); Phase 5 survivors (ChildField.SingleRecordTableField, *FromReturning, Reader.ResultRowWalk, the five FetcherEmitter.buildSingleRecord* methods, three remaining load-bearing keys) stay alive on the producer side until R180 lifts them. Out of scope (called out, not regressed): RecordBindingResolver walk reshape (R178 adds one new producer arm but does not change the resolver’s model), R156’s NodeId encoder chain, two-step DML emit shape, wire-format serialization, the @table-parent child-classification path. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R184 (05e21d5 Graphitron.newExecutionInput factory + getContextArgument default, 5d308c2 In Progress → In Review): collapses the two pieces of per-request boilerplate every graphitron app repeats (.graphQLContext(b -> b.put(GraphitronContext.class, ctx)) to thread the typed context key generated fetchers look up, plus .dataLoaderRegistry(new DataLoaderRegistry()) to satisfy graphql-java’s always-required registry) into a single generated factory entry point on the emitted Graphitron facade. GraphitronFacadeGenerator now emits two static newExecutionInput overloads alongside buildSchema: newExecutionInput(GraphitronContext context) returns ExecutionInput.newExecutionInput().graphQLContext(b -> b.put(GraphitronContext.class, context)).dataLoaderRegistry(new DataLoaderRegistry()), and the single-tenant convenience newExecutionInput(DSLContext dsl) delegates to newExecutionInput((GraphitronContext) env -> dsl). To make the lambda form bind to GraphitronContext’s SAM rather than infer as `Function<DataFetchingEnvironment, DSLContext>, GraphitronContextInterfaceGenerator flips getContextArgument from ABSTRACT to DEFAULT with body return env.getGraphQlContext().get(name); (matching the legacy DefaultGraphitronContext); getDslContext is now the only abstract method on the interface. The interface is deliberately not annotated @FunctionalInterface (a permanent contract that would block ever adding another abstract method); instead a pipeline-tier test pins the count of abstract methods to one, so any future generator change that adds a second abstract method fails the test in tandem with the sakila example’s compile of the lambda form. Tests: pipeline-tier GraphitronFacadeGeneratorTest asserts the two newExecutionInput overloads exist with (GraphitronContext) and (DSLContext) parameter lists, both returning graphql.ExecutionInput.Builder, both public static; pipeline-tier GraphitronContextInterfaceGeneratorTest asserts getContextArgument carries DEFAULT (not ABSTRACT) with the right body and that the emitted interface has exactly one abstract method; compilation-tier NewExecutionInputFactoryTest in graphitron-sakila-example pins three graphql-java contract facts (.dataLoaderRegistry(custom) replaces the factory’s fresh registry rather than merging ; exercises the user-visible override path; the (DSLContext) overload defaults to an empty registry; the (GraphitronContext) overload places the context under the typed GraphitronContext.class key generated fetchers read from). Execution coverage comes from the sakila example’s rewritten GraphqlResource.execute: the six-line builder collapses to three via Graphitron.newExecutionInput(new AppContext(...)), and every existing execution test in graphitron-sakila-example runs through that method. Docs updated to point at the factory (getting-started.adoc hello-world + multi-tenant + DataLoader-registry sections; runtime-extension-points.adoc registration snippet; emitted facade + interface javadocs). Out of scope (called out, not regressed): no federation overload of newExecutionInput (ExecutionInput carries no federation-specific wiring; the same factory serves both schema flavours); no change to DataLoader registration mechanics (generated fetchers continue to populate the registry lazily via computeIfAbsent); no new module, no change to dependency graph; getTenantId and getValidator defaults unchanged (they were already default-method shaped). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R179 (ef0af6c delete ResultAssembly; service success arm is universal passthrough, a3bde69 self-review cleanup: retire stale ResultReturnType coverage claim): deletes the ResultAssembly + ResultSlot model types, the resolveServiceResultAssembly + buildResultAssemblyBeanArm classifier in FieldBuilder, and the buildSuccessPayload / buildSuccessPayloadCtor / buildSuccessPayloadSetters emit helpers in TypeFetcherGenerator. The success arm collapses to a single universal-passthrough body: T result = service.method(...); return success(result);. The architectural constraint the deletion enforces is that the generator does not construct output DTOs on the happy path: per-field wiring (graphql-java child fetchers) projects SDL fields off the parent’s domain return, and a SDL-declared CreateFilmPayload { film: Film, errors: [Error] } does not need a Java twin. The catch-arm payloadFactoryLambda is the only remaining DTO-construction site, and the boundary is structural ; error-routed lists are produced inside the generator-owned try block where per-field wiring has no parent value to project from. The four service-backed Field records (Query/Mutation × Table/Record) lose their trailing Optional<ResultAssembly> resultAssembly component; the four arms in classifyQueryField / classifyMutationField route through buildServiceField, whose BiFunction<channel, assembly, ...> collapses to Function<channel, ...>. A new surviving classifier check (checkServiceReturnMatchesPayload in FieldBuilder) replaces the three Assembly-specific reject messages with the single legacy-passthrough wording: "@service method '<cls>.<method>' must return '<sdlPayloadTypeName>' to match the field's declared payload type — got '<method.returnType()>'". Stale-doc cleanup spans 12 sites (FieldBuilder LoadBearingClassifierCheck descriptions ×2 + ServiceCatalog.reflectServiceMethod description retired ResultReturnType coverage claim, FieldBuilder javadoc ×4, model-class javadocs on DefaultedSlot / PayloadConstructionShape / NonBoundSetter / ErrorsSlot, ServiceDirectiveResolver / MutationInputResolver ResultReturnType arm comments, SettKvotesporsmalShapeRegressionTest + TestServiceStub javadoc). R169 (service-domain-object-execute-coverage, Backlog) deletes in the same commit per workflow.adoc’s "supersession before shipping" Discarded pattern: its entire scope was execute-tier coverage for the now-deleted Assembly arm. One fixture addition beyond the spec’s enumeration: TestServiceStub.runSakWithInputBean(TestInputBean) returning SakPayload, replacing the String-returning stub that the pre-existing FetcherPipelineTest.inputRecord_validatorPreStep_* test had relied on the Assembly arm to admit; the new stub preserves the test’s intent (validator pre-step on Input-typed arg with VALIDATION-bearing channel) without losing the R150 input-bean classification dimension. LoadBearingGuaranteeAuditTest net stays balanced: both payload-construction.* producers retain surviving consumers (catch-arm payloadFactoryLambda + validator pre-step declareEarlyPayloadFromErrors / declareEarlyPayloadSetters); no producer becomes orphaned. Out of scope (called out, not regressed): inlining the success-arm local (kept for catch-arm try-block uniformity); redesigning the catch-arm payloadFactoryLambda path (the only remaining DTO-construction site, principled and unchanged). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R177 (1aba97f child @service rows-method preserves specific XRecord type, de5ed05 self-review: name the third consumer in RowsMethodShape class doc): narrows the child-@service emit site so the rows-method’s V and the matching DataLoader<K, V> value type both honor the TableBoundReturnType classifier guarantee instead of widening to raw org.jooq.Record. RowsMethodShape.strictPerKeyType returns tb.table().recordClass() for the TableBoundReturnType arm (was: a RECORD constant, now deleted); TypeFetcherGenerator’s `ChildField.ServiceTableField arm threads stf.returnType().table().recordClass() as the single servicePerKeyType local into both buildServiceDataFetcher (the DataLoader<K, V> typing line) and buildServiceRowsMethod (the Map<K, V> / List<V> return-type line), so the typed loader populates from the rows-method without a wildcard or defensive cast. ServiceDirectiveResolver.validateChildServiceReturnType’s `@LoadBearingClassifierCheck description is rewritten to name both emit-site consumers explicitly (rows-method .returns(...) and the typed DataLoader<K, V>), capturing that the strict TypeName.equals arm is now load-bearing for the typed loader’s compile via Java generics invariance ; not just structural symmetry; a new @DependsOnClassifierCheck on buildServiceDataFetcher pins the data-fetcher side to the same key, and the existing annotation on buildServiceRowsMethod is updated to reflect the narrower V. RowsMethodShape’s class-level docstring lifts the consumer count from two to three and names the DataLoader-typing line with the Java-generics-invariance reason. The diagnostic wording on the validator’s strict-return rejection shifts from `must return 'List<Record>' to must return 'List<LanguageRecord>' (the specific record class for the field’s bound table). Tests: six ServiceTableField unit assertions in TypeFetcherGeneratorTest flip from org.jooq.Record to the specific FilmRecord (positional + mapped, single + list, on both data-fetcher and rows-method return); three new R177-axis enum rows in GraphitronSchemaBuilderTest paired with three new fixtures in TestServiceStub cover the migration arm (List<List<Record>> was accepted, now rejected), acceptance arm (List<List<LanguageRecord>> was rejected, now accepted), and cross-record regression (List<List<FilmRecord>> stays rejected); CHILD_SERVICE_TABLE_BOUND_WRONG_RETURN_REJECTED’s diagnostic assertion updated to the narrowed wording; pipeline-tier `TestFilmService.getFilms tightened to List<List<FilmRecord>> with the paired pipeline assertion flipped; new positive compile-tier fixture Film.languageByService in graphitron-sakila-example backed by FilmService.languageByService returning Map<Record1<Integer>, LanguageRecord> makes mvn compile -pl :graphitron-sakila-example the load-bearing guarantee against future re-widening of the emit site. Out of scope (per spec, not regressed): ChildField.ServiceRecordField’s `elementType() fallback path (the asymmetry is principled ; ServiceRecordField carries the broad ReturnTypeRef sealed root rather than TableBoundReturnType, and strictPerKeyType can return null for that variant); the SourceKey.Wrap.TableRecord source-side typing pipeline (R177 brings the target side into alignment with what the source side already does); the rows-method’s outer container shape (Map vs List, single vs list cardinality ; outerRowsReturnType continues to wrap whatever perKey it’s handed); SplitRowsMethodEmitter (the ChildField.SplitTableField arm stays raw Record, explicit non-goal). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R96 (ab18e8c ProducerBinding sealed taxonomy + RecordBindingMismatch rejection, d098d76 RecordBindingResolver ; reflection-driven SDL → backing-class walker, bc1e457 wire RecordBindingResolver into TypeBuilder + reflection-derived dispatch, 2a19d40 transitional dispatch: gate result-axis walker on @record + directive fallback, 37ee435 flatten RecordBindingMismatch to RecordBindingMultiProducer + doc coverage, 4127f3d pipeline-tier tests for record-binding behaviour, ec229f1 self/arch review fixes): replaces the directive-driven recordBackingClasses population in TypeBuilder with a reflection-driven walk that grounds at root producers (@service returns, @table resolutions, @tableMethod returns) and propagates through parent-accessor return types to a fixed point. RecordBindingResolver.resolveAll() accumulates every observed binding per SDL type into a collection set keyed on either axis (result / input); the per-type set folds at the end of the walk into either an agreed Class<?> (singleton) or Rejection.AuthorError.RecordBindingMultiProducer (more than one distinct class). Cycle protection is fixed-point iteration over the per-type collection sets rather than recursive descent: each pass snapshots the currently-folded bindings, walks each parent’s accessor edges (getX / isX / x / public field-read, with optional DataFetchingEnvironment param), and adds new (reflectedClass, site) pairs to the per-type sets until a pass produces no new entry; a 1000-pass safety bound surfaces non-convergence as IllegalStateException. The new sealed ProducerBinding taxonomy (RootService / RootTable / RootTableMethod / ParentAccessor) carries the typed list inside the rejection so downstream tooling switches on the arm rather than parsing prose. Three-variant directive-ignored warning emitted at a single post-classification site (TypeBuilder.emitDirectiveIgnoredWarnings): Matches ("redundant; remove it") when the directive’s className equals the reflected class or the directive carries no className; Disagrees ("graphitron derives <X>") when they differ, naming the reflected class; Shadowed by @table (input types only) when @table co-occurs and grounds the binding. Variant precedence: Shadowed > Matches/Disagrees; a multi-producer rejection suppresses the warning entirely (error supersedes warning at the same site). The legacy @table + @record input-side warning at TypeBuilder.java:826-831 is removed; the redundancy signal is now carried by the Shadowed by @table variant. Load-bearing pin: producer @LoadBearingClassifierCheck(key = "record-binding.producer-agreement") on RecordBindingResolver (description names the two pure-function commitments riding under the check: ServiceCatalog.resolveTableByRecordClass derives TableRef purely from cls, and a Java record’s component list is a pure function of cls) pairs with @DependsOnClassifierCheck on FieldBuilder.resolveRecordAccessor. Tests: pipeline-tier R96RecordBindingPipelineTest (six cases: Matches with @service producer; Disagrees with directive lying about class; Shadowed-by-table on input; unreachable type falls back to directive’s className for backward compat; plain SDL carrier preserved through R75’s PojoResultType.NoBacking promotion; multi-producer disagreement surfaces typed RecordBindingMultiProducer rejection); RejectionSeverityCoverageTest adds the new permit to its sample factory; GraphitronSchemaBuilderTest’s `TABLE_PLUS_RECORD and SERVICE_WITH_RECORD_BACKING_CLASS_MISMATCH_REJECTED cases update to assert R96 semantics (table wins on input + Shadowed-by-table warning; service-with-record-mismatch corrects silently with Disagrees warning instead of UnclassifiedField); ErrorChannelClassificationTest.unTypedRecordPayload_* flips from "produces no channel" to "produces channel from reflected producer" ; a correctness improvement R96 introduces because @record without className now grounds via the producer’s return type rather than falling through to NoBacking; SealedHierarchyDocCoverageTest passes against the updated typed-rejection.adoc (new fourth AuthorError arm documented at chapter prose + mermaid class diagram). The sakila/test fixture corpus emits the expected Matches warnings (CreateFilmPayload, CreateFilmsPayload, CustomerAddressSummary, FilmLookupPayload, FilmReviewPayload, SetterShapeFilmReviewPayload) ; every @record-decorated reachable type whose directive’s className matches the producer’s return is now flagged as redundant. Transitional state (explicitly captured in the spec’s "Implementation notes" appendix before deletion, with the named follow-on tracked separately): walker’s @service result-axis observation is gated on the SDL return type carrying @record (preserves R75 single-record-carrier semantics; the post-retirement anchor is BuildContext.tryResolveSingleRecordCarrier, and the follow-on must flip the gate atomically with directive retirement); @tableMethod arm contributes input-axis observations only (the @table observation alone is sufficient for the result axis; obtaining the Table<Record> reflection class via TableImpl.recordType() for bare-class returns would add machinery without strengthening the diagnostic); buildResultType / buildNonTableInputType retain a directive-className fallback for types the walker can’t reach (so existing fixtures classify without mass migration); the rejection lands as a single Rejection.AuthorError.RecordBindingMultiProducer permit directly under AuthorError rather than a two-level RecordBindingMismatch.MultiProducer sub-taxonomy (the flat shape matches the rest of the AuthorError family and the typed List<ProducerBinding> payload already carries what a sub-arm would key off; the SealedHierarchyDocCoverageTest’s qualified-mention regex collides on two-level prefixes ; a known doc-coverage tooling fix tracked as a follow-on so future shapes that need sub-taxonomies aren’t blocked); drop-manifest golden file, validator-tier tests on the warning’s three variants, and the synthetic accessor-graph unit test on the resolver (diamond / deep chain / grounded cycle / ungrounded cycle) are deferred to the follow-on item, which will retire the directive-fallback path, migrate the remaining test fixtures, and ship the `directives.graphqls:290 directive declaration retirement atomically with the walker’s @record-gate flip once the warning count drops to zero across the corpus. Out of scope (per spec, not regressed): retiring the directive declaration itself; retiring any of the eight backed model variants (R96 changes the binding source, not the destination); the @service-payload error-construction surface (payloadFactoryLambda, ResultAssembly, PayloadAccessor); R94’s input-record validation seam (recordShape slot on the four InputType permits is graphitron-emitted validation class; R96’s recordBackingClasses is author-supplied accessor target ; orthogonal axes). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R176 (8292468 preserve upstream rejection in EntityResolutionBuilder): replaces the misleading @key requires a @table-bound type; '<T>' has no @table directive synthesised at EntityResolutionBuilder.java:108-114 for any non-Table/Node classification with two call-site-specific behaviours that codify the principle downstream stages enrich an upstream rejection or pass it through; they do not relitigate it. When gType instanceof UnclassifiedType (the type was already rejected upstream by TypeBuilder.unknownTableRejection, the @node keyColumns unresolved-column check, malformed KjerneJooqGenerator node-id metadata, or @node declared on a type without implements Node), the new code skips the demote outright and lets GraphitronSchemaValidator.validateUnclassifiedType surface the original cause unmodified. When gType is a genuine non-table-bound classification surviving the assembledType instanceof GraphQLObjectType and TableInterfaceType pre-checks (today: PlainObjectType and the ResultType sub-hierarchy JavaRecordType / PojoResultType / JooqRecordType / JooqTableRecordType), the demote fires with a kind-aware rejection: @key on type '<T>' requires a table-bound type, but '<T>' is classified as <kind> — federation entities need a @table directive. where <kind> is supplied by a new private kindLabel(GraphitronType) switch (PlainObjectType → "a plain object type", the four ResultType variants → "a @record type", default branch → "a non-table-bound type" for any future classification a contributor adds without updating the switch). The three legitimate demote callers (TypeBuilder.java:226 typeId collision, EntityResolutionBuilder.java:104 @key on TableInterfaceType, EntityResolutionBuilder.java:128 alternative-build error) all demote from classified entries and stay as-is; only the bug call site is gated. Why call-site, not a TypeRegistry.demote-refuses-overwrite invariant: rejection durability is the caller’s responsibility because only the caller knows whether it’s enriching a rejection the classifier couldn’t see (legitimate) or relitigating one (the bug); the registry can’t tell those apart from the type signatures alone, so the discipline lives at the call site where the knowledge lives. Tests: EntityResolutionBuilderTest adds three regression cases pinning the new behaviour (keyOnTypeWithUnresolvableTable_preservesUnknownTableRejection asserts the rejection contains could not be resolved in the jOOQ catalog and does not contain has no @table directive; keyOnNodeTypeWithUnresolvableKeyColumn_preservesUnresolvedColumnRejection asserts the rejection contains key column 'definitely_not_a_column' in @node could not be resolved and the same absence; keyOnRecordType_namesRecordKindInMessage asserts the rejection contains is classified as a @record type and the same absence), and tightens the existing plainObjectTypeWithKey_demotesToUnclassifiedType from a loose contains("@table") to require both is classified as a plain object type and federation entities need a @table directive. Out of scope (called out, not regressed): surfacing all rejections per type rather than the first (the validator’s one-error-per-UnclassifiedType policy is unchanged); LSP fix-it hints for the new wording (the structural Rejection carries enough payload for an LSP layer to consume later); changing TypeRegistry.demote semantics. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R94 (1224167 scope down emit shape to class-not-record (R172/R174 follow-ons filed), 418ef97 ship per-SDL-input-type validation class + rewired validator pre-step, e86c856 self-review cleanup: drop dead two-arg InputRecordGenerator.generate overload, simplify nested-input fromMap to one statement, refresh "Java record" → "Java class" Javadoc with R174 forward-looking notes): emits one graphitron-internal Java class per reachable SDL input type at <outputPackage>.inputs.<InputName> and rewires R12’s validator pre-step at TypeFetcherGenerator:1602+ to walk the typed instance instead of the raw Map. New HasInputRecordShape capability interface declared on the four InputType leaves (PojoInputType, JavaRecordInputType, JooqRecordInputType, JooqTableRecordInputType) and on TableInputType; InputRecordShape carries (recordClass, List<InputComponent>) with a compact constructor that rejects null/empty and backs the input-record.shape-from-input-type @LoadBearingClassifierCheck key. TypeBuilder.buildInputRecordShape walks SDL fields and lifts scalars via R101’s ScalarTypeResolver, enums to String (graphql-java’s wire shape), nested input refs to ClassName.get(<outputPackage>.inputs, name) (forward-declared ; javapoet does not require the class to exist at codegen, so mutually recursive inputs resolve cleanly), and lists to List<X>; a field whose scalar fails to classify routes the parent through UnclassifiedType via the existing fail-mode. InputRecordGenerator walks the reachable input closure off the assembled GraphQLSchema’s `GraphQLObjectType fields (the rewrite model’s RootType / TableBackedType don’t carry schemaType(), so the assembled schema is the authoritative source), expanding transitively through nested input components; each emitted class is public final with one private field per SDL component, a public same-name accessor, a private canonical constructor, a static fromMap(Map<String,Object>) factory (nested-input components recurse the sibling factory; list components stream element-wise; scalars/enums direct-cast; symmetric-null contract ; absent key and explicit null both collapse to a null component), and a per-class Javadoc tagging it as a graphitron-internal validation target. "inputs" is added to GraphQLRewriteGenerator.OWNED_SUBPACKAGES so the orphan sweep cleans regressions. TypeFetcherEmissionContext grows assembledSchema() + parentTypeName() so the rewired validator pre-step in TypeFetcherGenerator can resolve each SDL arg’s input-type-ness without re-walking the schema per arg; input-typed args materialise via <InputName>.fromMap(env.getArgument(name)) and feed validator.validate(<typed>). Scalar/enum args stay on the raw value path. Class-not-record by deliberate scope-down: graphitron-javapoet does not currently support TypeSpec.Kind.RECORD, sealed/permits clauses, or package-info.java emission (covers only CLASS, INTERFACE, ENUM, ANNOTATION); R174 (javapoet-record-sealed-package-info-support, Backlog) tracks the framework upgrade ; once it lands, InputRecordGenerator re-emits as actual records + sealed marker + package-info with no model-side ripple (InputRecordShape / InputRecordGenerator keep their names and semantics, only the rendered TypeSpec shape changes). The structural enforcement seam reduces to package boundary + per-class Javadoc for R94; R172 (inputs-package-internal-use-audit, Backlog) ships the service-side-reference audit independently. Hibernate Validator 9.0.1 walks records and beans identically for the property-path-from-component-name purpose R12’s ConstraintViolations.toGraphQLError needs, so the validator-walk function R94 delivers is preserved despite the source-form difference. Load-bearing pin: producer @LoadBearingClassifierCheck(key = "input-record.shape-from-input-type") on TypeBuilder.buildInputRecordShape pairs with @DependsOnClassifierCheck on InputRecordGenerator (per-input-type class emission) and TypeFetcherGenerator.validatorPreStep (typed-record materialisation in the pre-step rewire). Tests: pipeline-tier FetcherPipelineTest adds five R94 cases (inputRecord_scalar_emitsFromMapAndValidatesAgainstRecord, inputRecord_list_emitsListComponent, inputRecord_nested_recursesCoercer, inputRecord_unreachable_emitsNoRecord ; pins the reachable-closure scope decision via an unreachable input that produces no class ; and inputRecord_validatorPreStep_receivesTypedRecordNotMap ; the regression guard against drifting the pre-step back to validator.validate(Map)); unit-tier InputRecordGeneratorTest covers the emit shape (public class in <outputPackage>.inputs, fromMap factory with Map<String,Object> signature, one accessor per SDL component); InputTypeValidationTest gains a placeholderShape() helper since it constructs PojoInputType directly without the classifier. The validator pre-step walks the empty record (no constraints attached yet) ; the shape of the record (components, fromMap signature, walk-target) is exercised end-to-end on every fetcher with an input arg, so R98’s later content-attachment (programmatic ConstraintMapping entries) doesn’t have to reshape the record. R170 picks up the live invalid-input round-trip the moment R98 ships its first SDL constraint. Unblocks R98 (multi-source-input-validation, Backlog) ; the merged ConstraintSet’s programmatic-registration consumer gets its `mapping.type(InputRecord.class).field(componentName)... target ; and R170 (validator-integration-execute-coverage, Backlog) ; R12’s pre-step gains a real annotated walk target for the execute-tier ConstraintViolation round-trip fixture. Out of scope (called out, not regressed): exposing emitted classes to service signatures (R150 owns @service value flow via consumer-authored beans; the graphitron class is a validation target only and is discarded after validator.validate returns); replacing the Map.get() pattern in DML emitters (the four buildMutation{Delete,Insert,Update,Upsert}Fetcher paths and R75/R161-shipped DML-record paths keep their current shape ; the validate-only class runs in parallel at the fetcher boundary, value reads stay on the Map); destructuring @service callsites (R150 owns); service-side validator.validate calls (validation is a fetcher-boundary concern; the service never sees the graphitron class); designing the SDL validation directive set (R98 owns the curated @Range / @Size / etc.); narrowing/deprecating/removing @record on INPUT_OBJECT (R96 owns the reflection-derived backing-class binding; R94’s graphitron-emitted class at <outputPackage>.inputs.<InputName> lives at a separate Java identity from whatever class @record binds the input type to); retiring the four GraphitronType.InputType variants (R96 keeps the variants and reshapes how they’re populated); the @table + @record shadow rule at TypeBuilder:815-824 (untouched). Forward references: R164 (field-model-two-axis-pivot, Backlog) will repoint the validator-pre-step dispatch site into ValidationBuilder.OnInput-arm pattern matching once the field-model pivot lands; the substance of the pre-step (call <InputName>.fromMap, then validator.validate) is unchanged ; R94’s InputRecordShape / InputComponent are type-side (attached to GraphitronType.InputType), and ValidationBuilder is field-side (attached to Field), so the two axes don’t compete. R171 (input-like-type-sealed-parent, Backlog) tracks folding InputType ∪ TableInputType under a sealed InputLikeType parent so the capability declaration becomes one site instead of five; until R171 lands, a future sixth input-like variant added to GraphitronType.permits will not get a compile-time miss for HasInputRecordShape. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25 (1763 graphitron module tests + sakila compile-tier + execute-tier).

  • R9 (a4675bf Phase 1 pipeline, c38ea0f Phase 2 in-repo .md → .adoc, 562e732 Phase 3 alf/graphitron-landingsside absorption, aa3511e Phase 4 roadmap/by-theme/changelog/plans render, 7abec54 Phase 5a custom-domain cutover, b824207 In Review): graphitron.sikt.no is now built by Maven and deployed to GitHub Pages. /docs/ is a pom-packaged Maven module (graphitron-docs) wired into the rewrite reactor via <module>../docs</module>; download-maven-plugin fetches @sikt/sds-core and @sikt/sds-button from the npm registry tarball at pinned versions, maven-antrun-plugin flattens package/dist/index.css into target/staging/css/sds-{core,button}.css, maven-resources-plugin stages authored /docs/*.adoc plus /graphitron-rewrite/docs/*.adoc under architecture/, roadmap-tool render-adoc emits roadmap/{index,by-theme,changelog,plans/<slug>}.adoc, and asciidoctor-maven-plugin (in the default-on docs profile, opt-out via -P!docs) renders the merged tree to target/generated-docs/ with failIf severity=WARN so missing xrefs, missing includes, and unresolved attributes fail the build. CI splits across three workflows: rewrite-build.yml builds the rewrite reactor on PR and trunk push and adds trunk-only docs-build / docs-deploy jobs (actions/upload-pages-artifact + actions/deploy-pages@v4, concurrency: { group: pages, cancel-in-progress: false }); preview-docs.yml builds the docs site on every PR touching docs/**, graphitron-rewrite/docs/**, or graphitron-rewrite/roadmap/** and uploads the rendered tree as a workflow artifact; the legacy maven-build.yml stays unchanged on Java 21. Sikt Design System integration is build-time (pinned <sds.core.version> / <sds.button.version> properties, npm-registry tarball preferred over JSDelivr after the Claude Code Web sandbox returned 403 host_not_allowed), so the deployed Pages site has no runtime third-party dependency; sds-core’s `LICENSE.md is copied into target/staging/css/ for attribution, intra-Sikt scope on Sikt-owned domain authorises the redistribution. The custom-domain cutover (Phase 5a, 7abec54) dropped the /graphitron/ path-prefix Phase 1 anticipated; Sikt platform team handled DNS and the Pages-settings custom-domain config; no /docs/CNAME file shipped (custom domain set via Pages settings, the standard mechanism with actions/deploy-pages). Phase 5b (K8s deployment retire, GitLab CI pipeline retire, alf/graphitron-landingsside archive) landed external to this repo and is confirmed complete. Done-commit housekeeping (<this commit>): cleared four user-facing-doc-check leaks the independent In Review reviewer (session_011jbm5PpFDrqu3WjhtXDFB4) surfaced ; docs/index.adoc:69 dropped the R68 scaffold-only second sentence (the manual is shipped and populated), docs/manual/reference/directives/externalField.adoc:88 and docs/manual/how-to/computed-fields.adoc:152 reframed the computed-field-with-reference.md plan-slug references to feature-status notes, docs/manual/reference/directives/value.adoc:39 softened "deferred to a follow-up roadmap item" to "UPSERT generation is deferred". Pre-existing drift in CLAUDE.md:74 (Documentation site section) and docs/README.adoc:6 rewrote the stale .github/workflows/deploy-docs.yml reference to point at the docs-build / docs-deploy jobs in rewrite-build.yml plus preview-docs.yml for PR previews. Build green: mvn -f graphitron-rewrite/pom.xml -pl :graphitron-docs -am package renders the merged tree under the WARN-fails policy on Java 25.

  • R167 (ee06817 unify schema file extension handling between schemaInputs and graphitron:dev): centralises the "what counts as a schema file" decision onto a single <schemaFileExtensions> Mojo parameter and threads it through RewriteContext.schemaFileExtensions(): Set<String> to three consumer sites that previously drifted independently. SchemaInputExpander.expand post-filters scanner matches by extension; SchemaWatcher’s schema-mode constructor takes the `Set<String> instead of a hard-coded .graphqls; SchemaProblemDiagnostic.findOrphanSchemaFiles reads the configured set instead of its own hard-coded .graphql/.graphqls predicate. The seven- and six-arg RewriteContext overloads default the new field to Set.of(".graphqls", ".graphql") so unit-tier callers stay one-liners; AbstractRewriteMojo.effectiveSchemaFileExtensions() is the normalisation seam (trim, leading-dot prepended, duplicates collapsed, empty-after-normalisation rejected with a MojoExecutionException). The default matches the orphan scanner’s pre-R167 behaviour, so consumers with .graphql files (Opptak’s regelverkMutations_exp.graphql was the concrete pain point) get the graphitron:dev watcher firing on save and the <schemaInputs> glob-expansion picking the file up without configuring anything; teams reserving .graphql for client query documents opt in to the tighter <schemaFileExtensions><extension>.graphqls</extension></schemaFileExtensions> policy. SchemaWatcher’s single-string-suffix constructor stays for the `.class classpath watcher (DevMojo.startClasspathWatcher still passes ".class"). Tests: unit-tier SchemaFileExtensionsNormaliserTest (6 cases: null returns default, missing-dot prepended, duplicates collapsed, whitespace trimmed, all-blank rejected, explicitly-empty rejected); SchemaInputExpanderTest adds expand_filtersFilesNotMatchingConfiguredExtensions, expand_dotGraphqlAccepted, and expand_zeroMatchAfterExtensionFilter_throwsMojoExecutionException; SchemaWatcherTest adds dispatch_triggersOnDotGraphql_whenConfigured, dispatch_ignoresUnconfiguredSuffix, and constructor_emptySuffixSet_rejected; SchemaProblemDiagnosticTest adds findOrphanSchemaFiles_respectsConfiguredExtensions (tighten + loosen both checked). docs/manual/reference/mojo-configuration.adoc documents the new parameter in the per-parameter reference table and updates the dev goal description; the <schemaInput> row prose softens from .graphqls files to "schema files (or globs that expand to schema files)". Out of scope (called out, not regressed): dropping the trailing /*.graphqls off <pattern> declarations so patterns describe directories only (purely additive on existing patterns); case folding (Linux is case-sensitive, .GraphQLs stays as authored); bundled directive files (directives.graphqls is a classpath resource, not consumer-configured). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R158 (f35644f admit @service-backed producers for single-record DML carrier data fields; da25606 pipeline + unit tests; 129909c execution-tier single-PK + ONE + empty + null-source cases; 08acc84 execution-tier MANY-arm composite-PK case): widens the single-record DML carrier’s data-field permit to admit a second producer kind alongside the existing INSERT/UPDATE/UPSERT DML mutation: an @service-backed mutation whose return type IS the carrier-payload’s typed XRecord / List<XRecord>. Closes the runtime ArrayList cannot be cast to org.jooq.Result reproducer (OpprettRegelverksamlingPayload + opprettRegelverksamling @service returning List<RegelverksamlingRecord>): pre-R158 the data-field fetcher cast env.getSource() to Result<RecordN<PK>> unconditionally, which holds for the DML mutation fetcher’s .returningResult(PK) shape but not for the developer’s verbatim list-of-typed-record return. SourceKey.Reader.ResultRowWalk’s compact-constructor invariant widens from `Wrap.Record + empty path to Wrap.Record OR Wrap.TableRecord(target.recordClass()) + empty path; the load-bearing key renames in lockstep to source-key.result-row-walk-target-aligned-empty-path (one @LoadBearingClassifierCheck on SourceKey and one @DependsOnClassifierCheck on FetcherEmitter updated, plus two javadoc references on ChildField.java / SourceKey.java). FetcherEmitter.buildSingleRecordTableFetcherValue becomes a sealed switch over SourceKey.Wrap permits: the Wrap.Record arm preserves the existing (Result<RecordN<...>>) env.getSource() / (RecordN<...>) env.getSource() casts and source.getValues(<PK>) / source.value1() reads unchanged; the new Wrap.TableRecord arm casts env.getSource() to (List<XRecord>) (MANY) or (XRecord) (ONE) and reads PKs through the typed record.get(<XTable.<PK_FIELD>>) accessors (single-PK uses the column’s Java type as map key; composite-PK uses List.of(r.get(pk1), r.get(pk2), ...) for map-keying and DSL.row(pk1, pk2).in(source.stream().map(r -> DSL.row(...)).toList()) for the response predicate); Wrap.Row is the unreachable arm pinned by an IllegalStateException. Registration moves to per-producer helpers. GraphitronSchemaBuilder.registerCarrierDataField’s `DataElement.Table arm hollows out; two new helpers in FieldBuilder are the only writers: registerDmlCarrierDataField (called from the non-DELETE DML kind classifier with Wrap.Record) and registerServiceCarrierDataField (called from the Resolved.Result arm of @service resolution with Wrap.TableRecord(target.recordClass())). The @service helper does its own strict method.returnType().equals(expectedReturnType) check against the carrier walk’s target.recordClass(), colocated because ServiceDirectiveResolver.computeExpectedServiceReturnType returns null for carrier-payload return types by design. Orphan carriers (a carrier type returned only from a Query field with no producing mutation) now land with no fieldRegistry entry, structurally safe under graphql-java’s never-traverse-an-unproduced-field guarantee. R156’s registerDeleteCarrierDataField Table arm passes null for expectedExistingClass in lockstep because the verbless walk no longer pre-registers; FieldRegistry.reclassify’s `expectedExistingClass parameter loosens to admit null (admits both no-prior-entry and matching-prior-entry once the helper-side compare-then-write has confirmed wrap agreement). Producer-kind monomorphism: a new BuildContext.carrierProducerRegistry map, keyed by (carrierType, dataFieldName) coords, records the first mutation that registers a SingleRecordTableField at each coord; the second producer’s helper reads it to enrich the rejection diagnostic when wrap shapes disagree (mixing a DML mutation and an @service mutation on the same carrier type rejects at classify time, naming both producer mutations regardless of registration order). The rejection routes through the standard UnclassifiedField + Rejection.structural + validateUnclassifiedField path; no parallel validator-mirror walk is needed. Load-bearing pins: producer @LoadBearingClassifierCheck(key = "carrier-data-field.single-producer-kind") on FieldBuilder.registerDmlCarrierDataField pairs with @DependsOnClassifierCheck on FetcherEmitter.buildSingleRecordTableFetcherValue (the wrap-permit dispatch relies on at most one wrap shape reaching the emitter per coord); producer @LoadBearingClassifierCheck(key = "carrier-data-field.service-producer-strict-return") on FieldBuilder.registerServiceCarrierDataField is a hygiene-rejection check (no consumer annotation owed; the cast safety it backstops is already pinned by source-key.result-row-walk-target-aligned-empty-path via the Wrap.TableRecord(target.recordClass()) invariant). Tests: unit-tier SourceKeyTest (six cases pinning the loosened Reader.ResultRowWalk compact-constructor invariant: Wrap.Record and Wrap.TableRecord(target.recordClass()) admit; cross-table Wrap.TableRecord(other) rejects with target-aligned message; non-empty path rejects under either admitted wrap; Wrap.Row rejects); pipeline-tier SingleRecordTableFieldServiceProducerPipelineTest (eight cases: ONE/single-PK, MANY/single-PK, and MANY/composite-PK FilmActor admission pins the Wrap.TableRecord(target.recordClass()) shape and registered SourceKey.columns; wrong-element-type, Set<XRecord>, Iterable<XRecord> reject through the strict-return predicate; mixed-producer DML-first and @service-first rejection asserts both producer mutation names and both wrap shapes appear in the diagnostic); execution-tier SingleRecordTableFieldServiceProducerExecutionTest against native PostgreSQL (five cases on Sakila FilmCarrierService / FilmActorCarrierService fixtures: MANY-arm single-PK input-order preservation through the R141 PK-keyed-map walk; MANY-arm composite-PK input-pair-order preservation exercising the typed row(pk1, pk2).in(...) predicate emission and List.of(r.get(pk1), r.get(pk2)) map-key shape unique to multi-column keys; empty-source short-circuit; ONE-arm end-to-end; ONE-arm @service returns null → graphql-java does not traverse the carrier, payload renders null end-to-end); GraphitronSchemaBuilderTest’s `SINGLE_RECORD_CARRIER_DATA_FIELD repurposes as _ORPHAN (Query-rooted carrier with no producing mutation: assert no fieldRegistry entry); SingleRecordCarrierPipelineTest.carrier_returnedFromQueryField_* retargets to assert the orphan no-registration invariant. Out of scope (called out, not regressed): R141’s PK-keyed-map → VALUES-idx-JOIN migration (working code with its own audit surface; refactor of working code, not part of producer admission); Reader.ResultRowWalk consumed outside SingleRecordTableField (the widened invariant pairs only with the carrier data field’s permit today; any future consumer must adopt the same wrap-dispatch pattern or split its own permit); @service producer with DataElement.Record data field (identity-passthrough permit SingleRecordIdentityField is producer-kind-irrelevant because the data field’s value IS the parent’s, regardless of producer). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R12 (carrier-walk LocalContext phases: eb590ef split ErrorChannel into sealed PayloadClass | LocalContext; c8731c9 add Transport discriminator on ChildField.ErrorsField; 676ff72 producer wiring for CarrierFieldRole.ErrorChannelRole; 04799b8 emit ErrorRouter.dispatchToLocalContext; d7d1c55 wire emit-time fork for ErrorChannel.LocalContext; 9cce63f select ErrorsField.Transport from parent’s resolved channel; a96766d register ErrorsField on carrier-walk carriers + MappingsConstantNameDedup arms; 093779c validator mirror for LocalContext errors-channel guard; f3ddcd4 thread non-null sentinel through LocalContext catch path + pipeline / execute tests; 93936d8 anchor null-source guard sites to validator allow-list via audit annotations; 4f1333f refresh spec body + split open execute-tier fixtures to R169 / R170): emit per-fetcher error channels from @error. Closes the long-standing "no generation (error mapping config)" gap so a payload’s errors: [SomeError!] field now routes a thrown exception into the typed payload instead of leaking the raw exception message through graphql-java’s default SimpleDataFetcherExceptionHandler. Foundational pieces landed first (sealed Handler taxonomy ExceptionHandler | SqlStateHandler | VendorCodeHandler | ValidationHandler on GraphitronType.ErrorType.Handler with parse-time lift and rules 1-6; ChildField.ErrorsField admitting where the five PolymorphicReturnType rejection arms used to fire; the ErrorChannel carrier with mappedErrorTypes, payloadClass, errorsSlot, defaultedSlots, mappingsConstantName; classify-time MappingsConstantNameDedup with 8-hex SHA-256 collision suffix; channel-level rule 7 multi-VALIDATION + rule 8 duplicate-criteria checks; ErrorRouter.dispatch / redact / Mapping taxonomy emitted at <outputPackage>.schema.ErrorRouter with ErrorMappings constants alongside; (List<String>, String)-ctor classifier check via Class.forName; per-@error-union/interface TypeResolver registration and per-@error-type path / message field DataFetchers in GraphitronSchemaClassGenerator; source-direct dispatch with no developer @error Java backing class; Optional<ErrorChannel> slot on every WithErrorChannel permit; per-fetcher try/catch wrapper + .exceptionally async tail routing the catch arm through ErrorRouter.dispatch or ErrorRouter.redact; DML payload assembly + dispatch; ResultAssembly for service-side payload assembly; rule 6 relaxation + per-(channel, @error type, handler) source-class accessor reflection check via R88’s ClassAccessorResolver; extensions.constraint field population on ConstraintViolations.toGraphQLError; child @service / @tableMethod errorChannel lift; @service / @tableMethod declared-exception channel-coverage check). The In Review pass shipped the carrier-walk LocalContext story (the R161 enabler): ErrorChannel is now a sealed PayloadClass | LocalContext interface with the channel-agnostic mappedErrorTypes() / mappingsConstantName() accessors; the BuildContext.classifyCarrierField producer admits errors-shaped wrappers as CarrierFieldRole.ErrorChannelRole with a LocalContext binding ahead of DataChannel resolution; ChildField.ErrorsField grows a Transport transport() component (PayloadAccessor | LocalContext) selected at classify time from the parent’s resolved channel; TypeFetcherGenerator.catchArm and asyncWrapTail switch exhaustively on the sealed root and emit ErrorRouter.dispatchToLocalContext(...) for the LocalContext arm. The runtime fix threads a typed P sentinel through dispatchToLocalContext: graphql-java’s completeValueForObject short-circuits children on a null parent, so the catch arm now packs data(sentinel).localContext(List.of(t)).build() where sentinel = DSL.using(SQLDialect.DEFAULT).newRecord(<pk>) (single) / newResult(<pk>) (bulk); the data field’s null-source guard renders the SDL response as data: null while the errors field reads via env.getLocalContext(). The catchArm helper carries a generator-internal 3-arg overload that throws when a LocalContext callsite forgets the sentinel. Load-bearing pin: producer @LoadBearingClassifierCheck(key = "error-channel.local-context-transport") on BuildContext.classifyCarrierField pairs with @DependsOnClassifierCheck on TypeFetcherGenerator.dispatchToLocalContextCatchArm, the Transport.LocalContext arm of FetcherEmitter.dataFetcherValue, FieldBuilder.transportForParent, the validator mirror GraphitronSchemaValidator.validateLocalContextErrorsFieldGuards, and the four per-variant emitter sites (buildSingleRecordTableFetcherValueRecordWrap / ...TableRecordWrap, buildSingleRecordIdFromReturningFetcherValue, buildSingleRecordTableFromReturningFetcherValue, buildSingleRecordIdentityFetcherValue) that anchor each if (source == null) return null; guard to the audit harness. The validator mirror rejects schemas whose ErrorsField carries Transport.LocalContext but whose sibling data-channel field is outside the LOCAL_CONTEXT_GUARDED_DATA_CHANNEL_VARIANTS allow-list, turning a future widening that admits a non-guarded variant into a build-time Rejection.AuthorError.Structural rather than a request-time NPE. Tests: unit-tier ErrorRouterClassGeneratorTest (13 cases pinning dispatchToLocalContext(thrown, mappings, env, sentinel) signature + body packs sentinel into data()); pipeline-tier SingleRecordCarrierPipelineTest (34 cases including the three R12 + R161 integration tests: single-input MutationDmlRecordField and bulk-input MutationBulkDmlRecordField each classify with errorChannel = Optional.of(LocalContext) and a sibling ErrorsField with Transport.LocalContext; emit pin for SQLDialect.DEFAULT / newRecord sentinel construction and env.getLocalContext() reading); pipeline-tier ErrorChannelClassificationTest (21 cases including carrier-walk LocalContext admission + rule 7 multi-VALIDATION rejection through the unified walk); validator-tier LocalContextErrorsFieldValidationTest (3 cases: guarded sibling passes, unguarded sibling rejects with allow-list diagnostic, PayloadAccessor ErrorsField with unguarded sibling untouched); audit-tier LoadBearingGuaranteeAuditTest picks up the producer + all consumers via the annotation scan; execute-tier GraphQLQueryTest adds Sakila SDL fixtures FilmCreateLocalContextPayload + FilmCreateConstraintViolation @error type (handler GENERIC against org.jooq.exception.IntegrityConstraintViolationException) with two end-to-end paths: valid languageId=1 round-trips with {film: {...}, errors: null}; languageId=99999 trips PostgreSQL FK 23503, routes through dispatchToLocalContext, renders {film: null, errors: [{__typename: FilmCreateConstraintViolation, path: [...], message: "...foreign key..."}]}. R2 retirement: checked-exceptions-typed-errors.md (Backlog R2) is subsumed by §4’s declared-checked-exception channel-coverage check and can be retired. Out of scope (called out, not regressed): execute-tier coverage for the @service ResultAssembly.Assembly arm (split to R169, not blocked); execute-tier coverage for the Jakarta ValidationHandler channel (split to R170, blocked on R94 emit-input-records); subscription error paths; batch-loader per-key error handling; federation entity-resolver errors; instrumentation hooks; transaction rollback semantics; consumer-facing ExceptionHandlingBuilder analogue (auto-wiring is the goal, no top-level handler to install); custom ExecutionStrategy for non-error reasons. Behaviour shifts vs legacy (documented in user-facing migration table): IllegalArgumentException messages are no longer automatically exposed to clients (schemas relying on the legacy auto-leak must declare {handler: GENERIC, className: "java.lang.IllegalArgumentException"}); DATABASE handlers now match any SQLException in the cause chain, not only those wrapped in Spring’s DataAccessException (non-Spring apps no longer need spring-jdbc for database error mapping). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R161 (d0f676c retire DmlReturnExpression.Payload and unify @record-returning DML on the carrier-walk path; 8a9a707 lift the NoBacking/ClassBacked fork into SingleRecordCarrierResolution.Ok): collapses the two parallel @record-returning DML designs onto Path 1. BuildContext.tryResolveSingleRecordCarrier’s candidate predicate widens from `{PlainObjectType, PojoResultType.NoBacking} to {PlainObjectType, ResultType} so every @record(record:{className:}) wrapper (Backed, JavaRecordType, JooqRecordType, JooqTableRecordType) routes through MutationDmlRecordField / MutationBulkDmlRecordField uniformly; Mutation*TableField permits are now guaranteed never to carry a @record return, enforced structurally rather than via classifier-acceptance shape. The DmlReturnExpression sealed type collapses to four arms (EncodedSingle / EncodedList / ProjectedSingle / ProjectedList); the PayloadAssembly / RowSlot model types, the reflection-based resolver (resolveDmlPayloadAssembly, buildDmlPayloadAssemblyBeanArm, DmlPayloadAssemblyResult, NO_ASSEMBLY), and the emit layer (emitPayload, emitPayloadCtor, emitPayloadSetters) all retire. MutationInputResolver.validateReturnType drops the fqClassName == null guard on the ResultReturnType arm so the carrier-walk probe runs unconditionally ; one probe over the SDL shape, not two probes composing. The follow-up commit lifts the consumer-side parentType instanceof PojoResultType.NoBacking re-narrowing at GraphitronSchemaBuilder:227 into the model as a sealed Ok interface with NoBacking / ClassBacked record sub-arms (BuildContext.tryResolveSingleRecordCarrier tags the outcome via target instanceof ResultType && !(NoBacking)), per Generation-thinking: type-level classification short-circuits to carrier-walk registration only on NoBacking; ClassBacked falls through to normal per-type classification so R88’s per-field accessor-resolution diagnostics surface on developer-supplied classes, and the mutation classifier reclassifies the data field via compare-then-write at mutation time. code-generation-triggers.adoc’s Mutation Fields trigger table is qualified ("returning ID or a `@table type" on the four Mutation*TableField rows) and gains two new rows for MutationDmlRecordField / MutationBulkDmlRecordField; the .returningResult(pkCols) design decision is captured on buildMutationDmlRecordFetcher and buildMutationBulkDmlRecordFetcher (PK-only RETURNING keeps the write transaction minimal; data-field projection runs in a separate read-only follow-up SELECT outside the transaction). LoadBearingClassifierCheck descriptions on resolvePayloadConstructionShape and buildDmlField are trimmed to reflect the post-R161 consumer set (DML-row emitPayload removed; four-arm DmlReturnExpression). Tests: pipeline-tier DML_RECORD_PAYLOAD_RETURN_HAPPY and DML_RECORD_PAYLOAD_ROW_ONLY_HAPPY re-target as carrier-walk admission tests (DML_RECORD_CARRIER_WITH_ERRORS_HAPPY / DML_RECORD_CARRIER_ROW_ONLY_HAPPY) exercising the JavaRecordType wrapper arm admitted by the R161 widening; DML_RECORD_PAYLOAD_NO_ROW_SLOT_REJECTED repurposes as DML_RECORD_CARRIER_NO_DATA_CHANNEL_REJECTED (the carrier walk rejects the SDL shape for missing a DataChannel field rather than reflecting on the developer’s class); DML_RECORD_PAYLOAD_LIST_REJECTED keeps its name with the rejection diagnostic now coming from validateReturnType instead of resolveDmlPayloadAssembly. FetcherPipelineTest’s `dmlMutation_setterShapePayload_emitsSetterFactory, dmlDeleteField_recordPayloadReturn_successArmConstructsPayloadAndCatchArmDispatches, dmlDeleteField_recordPayloadReturnNoErrorsField_successArmConstructsPayloadCatchArmRedacts, and the unused SetterShapeDeleteFilmPayload fixture all delete (the bodies they asserted on no longer exist). After migration: zero references to DmlReturnExpression.Payload, PayloadAssembly, RowSlot, emitPayload*, or resolveDmlPayloadAssembly anywhere in the codebase. Out of scope (called out, not regressed): execution-tier coverage for the four ResultType className-carrying arms (sakila’s FilmPayload already pins the carrier-walk emit shape via NoBacking, and post-R161 every wrapper state runs the same emitter code, so a bespoke fixture would assert structural rather than behavioral coverage); consolidating MutationField permits under verb-on-permit-identity (MutationInsertResultField / etc., tracked at R162). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R39 (f8fc604 wip validator + tests; d0cf6b5 validator ship + carrier permit lift; 2600955 lift PK-derived orderBy to OrderingOwnedByProducer sealed marker): Cross-cutting validator rejects SqlGeneratingField + FieldWrapper.List + OrderBySpec.None at build time, closing the silent-non-determinism gap on list fields targeting no-PK tables (OrderByResolver.resolveDefaultOrderSpec falls back to OrderBySpec.None when no @defaultOrder/@orderBy is present and the target has no PK, which generators faithfully emit as List.of() ; no ORDER BY ; producing visibly different row order each run). Three checks now cover three disjoint shapes: validatePaginationRequiresOrdering (paginated, including connections), validateSplitTableField connection branch (@splitQuery connections), validateListRequiresOrdering (plain [T] list wrapper). Gated on FieldWrapper.List not wrapper().isList() so the three messages stay non-overlapping; the sealed FieldWrapper permit list (Single/List/Connection) is the typed gate. Marker carrier: a new sealed OrderingOwnedByProducer (model package) permits ChildField.SingleRecordTableField and ChildField.ServiceTableField; the validator excludes marker-bearing permits by type. The carrier permit’s structurally-empty orderBy() = None stays ; the visible result order on these permits is owned by an upstream producer (FetcherEmitter’s PK-keyed-map walk for the R141/R158 carrier; the developer’s `@service method for ServiceTableField), not by the field’s own orderBy() component. An earlier shape derived a PK-fixed OrderBySpec.Fixed from sourceKey.columns() inside SingleRecordTableField.orderBy() to side-step the validator; principles-architect flagged this as duplicating OrderByResolver.resolveDefaultOrderSpec’s no-directive branch and coupling the validator’s correctness to a `FieldBuilder classifier guarantee that no involved file named. The marker refactor moves the exemption into the type system: find-usages from either permit lands on the validator’s exclusion site, and adding a new permit to the sealed marker is the explicit deliberation point. The check is hygiene-rejection (no emitter relies on it; TypeFetcherGenerator.buildOrderByCode / buildConnectionOrderingBlock / buildBaseReturnExpr and InlineTableFieldEmitter defensively handle None/empty Fixed for Single and Connection-without-pagination shapes the validator does not gate), so no @LoadBearingClassifierCheck/@DependsOnClassifierCheck pair is owed. Tests: unit-tier ListRequiresOrderingValidationTest (5 cases pinning Query-rooted + child-position dispatch path × list/single × ordered/unordered); pipeline-tier ValidateListRequiresOrderingPipelineTest (reject + admit through SDL → classified model → GraphitronSchema on the Sakila no-PK film_list fixture, asserting the contract’d error message); five pre-existing tests ({Lookup,QueryLookup,RecordTable,RecordLookupTable}FieldValidationTest) updated to use PK-fixed orderBy where they incidentally used OrderBySpec.None + FieldWrapper.List ; in every case the test’s subject is FK paths / projection / cardinality, not ordering. Marker-exemption coverage rides on the existing R141 / R158 execution-tier sakila tests (FilmsPayload, FilmsServicePayload, FilmActorsServicePayload): each declares a list-shaped data field with no @defaultOrder; the validator must admit them or the entire suite fails to build. Out of scope (called out, not regressed): requiring ordering on single-value fields (no-op); changing OrderByResolver to refuse OrderBySpec.None (the validator is the right layer for "legal in the model but illegal as authored schema"); merging the cross-cutting checks (remediation text deliberately differs). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R165 (28cea70 gate empty fetcher-registration bodies at construction): FetcherRegistrationsEmitter.emit was producing empty-body entries for unreferenced payload-shaped types (the field-report reproducer was an unreferenced SlettRegelverksamlingPayload { regelverksamlingId: [ID!] @nodeId }), which surfaced as a javac error in consumer projects: GraphitronSchema.build() emitted <Name>Type.registerFetchers(codeRegistry) for every key in the bodies map, while ObjectTypeGenerator skipped the method emission when the body was empty. Fix gates emptiness at the construction site rather than via a post-pass scrub: typeBody and nestedBody return Optional<CodeBlock> (empty when the classified-field list is empty), and the two put call sites in emit use .ifPresent(body -> result.put(name, body)). The dead fields.isEmpty() short-circuit inside buildBody is removed (typeBody guards before calling). ObjectTypeGenerator’s `fetcherBody != null && !fetcherBody.isEmpty() gate collapses to a null check (the !isEmpty() half is dead under the new invariant; the null half still guards types absent from the keyset). Load-bearing pair under key fetcher-registrations.no-empty-bodies pins the producer-side guarantee: @LoadBearingClassifierCheck on FetcherRegistrationsEmitter.emit, matching @DependsOnClassifierCheck on GraphitronSchemaClassGenerator.generate’s keyset iteration; `LoadBearingGuaranteeAuditTest catches future drift as an orphaned-consumer audit failure rather than a downstream javac error. Tests: unit-tier FetcherRegistrationsEmitterTest (post-condition that emit’s returned map has no empty `CodeBlock values, plus key-absence on the bug-reproducing payload fixture, across single-record carrier and connection/edge code paths); pipeline-tier FetcherRegistrationsPipelineTest asserts the bi-directional set-equality invariant between <Name>Type.registerFetchers(codeRegistry) call sites in the emitted GraphitronSchema.build() body and the type names whose ObjectTypeGenerator TypeSpec declares a registerFetchers method, on both the field-report fixture and a realistic mixed fixture (pinning both directions catches drift whichever side of the keyset/method contract moves). Out of scope (called out, not regressed): reachability-based pruning of unreferenced SDL types (the orphan payload survives via additionalTypes); strengthening emit’s return type to a `FetcherBodies record or Map<String, NonEmpty<CodeBlock>> carrier (R166 floats the broader GraphQLSchemaVisitor-driven emission rework where this would land). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R154 (Phase 1 model lift + Phase 2 setter-shape admission, both landed before history-squash; landing SHAs unavailable post-squash): Admit a second @service payload construction shape ; public no-arg constructor + per-SDL-field Java-bean setters ; alongside today’s canonical all-fields constructor. New sealed PayloadConstructionShape { AllFieldsCtor | MutableBean } carries the contract; MutableBean.bindings holds one SetterBinding(sdlFieldName, setter, acceptsOptional) per SDL field in declaration order. The three carrier slot types lift in lockstep: ErrorsSlot, ResultSlot, RowSlot each become sealed with CtorParameterIndex(int) + SetterMethod(boundSetter, List<NonBoundSetter>) permits (kept as three sibling hierarchies, not folded onto one broad Slot interface, so ErrorChannel / ResultAssembly / PayloadAssembly keep role-specific access without instanceof widening). New NonBoundSetter(setter, defaultLiteral) record carries each non-bound SDL field’s setter paired with its language-default literal, so the catch-arm payload-factory emit walks one structured list and prints each setter call with its default value. Classifier: FieldBuilder.resolvePayloadConstructionShape(payloadCls, sdlFieldNames) returns a sealed PayloadConstructionShapeResult { Resolved(shape) | Reject(reason) }; predicates run in order with AllFieldsCtor first (canonical-over-bridge precedence: records always present the all-fields ctor; the setter shape is a legacy bridge from graphitron-codegen-parent); a class supporting both shapes resolves to AllFieldsCtor; the only rejection mode is neither-predicate-matches, with structured guidance enumerating the three escape hatches (convert to record / remove extra ctors / add no-arg + Java-bean setters). Three resolvers (resolveErrorChannel, resolveServiceResultAssembly, resolveDmlPayloadAssembly) consume the sealed shape and split into ctor-arm + bean-arm builders. Emit: TypeFetcherGenerator’s three payload-factory sites (catch-arm `errors -> ... lambda, service-result success arm, DML-row success arm) dispatch on PayloadConstructionShape via exhaustive sealed switch; the bean arm emits var p = new Payload(); p.setBound(...); p.setOther(<default>); ...; return p; instead of new Payload(...). Load-bearing pins: producer-side @LoadBearingClassifierCheck annotations on resolvePayloadConstructionShape for the keys payload-construction.shape-resolved (carrier-arm-totality) and payload-construction.setter-name-matches-sdl-field (setter.getName() is callable into the generated source); consumer-side @DependsOnClassifierCheck on the three emit-site forks plus the carrier helpers. Tests: unit-tier PayloadConstructionShapeTest (7 cases pinning record→AllFieldsCtor, bean→MutableBean, both-shapes→AllFieldsCtor canonical wins, missing-setter rejection naming the offending field, multi-ctor-no-no-arg→Reject, Optional<T> setter sets acceptsOptional, camelCase SDL field resolves through Java-bean naming xRating → setXRating); pipeline-tier FetcherPipelineTest four R154 cases (serviceMutation_setterShapePayload_emitsSetterFactory, _allFieldsCtorPayload_emitsCtorFactory_unchanged regression, _bothShapesPresent_prefersCtorFactory, dmlMutation_setterShapePayload_emitsSetterFactory); compilation-tier graphitron-sakila-service adds SetterShapeFilmReviewPayload (no-arg ctor + setReviewId / setErrors) plus FilmReviewService.submitSetterShape returning that type; sakila-example schema adds submitSetterShapeFilmReview mutation; execution-tier GraphQLQueryTest two end-to-end cases against real PostgreSQL (submitSetterShapeFilmReview_validInput_returnsHappyPathPayload and _invalidRating_routesThroughBadRatingErrorType) round-trip both the success-arm and the error-arm catch-arm payload-factory. Out of scope (called out, not regressed): builder-pattern (fluent immutable) payload classes (Payload.builder()....build()) ; a separate BuilderPattern permit on PayloadConstructionShape if a real schema surfaces it; replacing AllFieldsCtor with MutableBean (the two coexist; records remain the recommended shape); designing a @constructionShape(setter) SDL directive to disambiguate (per configuration drift reasoning, the structural signal the classifier already sees is sufficient); a deprecation diagnostic when a bean-shape payload could be a record (the parallel-support window has no deprecation pressure). Minor housekeeping noted at In Review → Done: SetterBinding.acceptsOptional is captured by the predicate but not consumed by any emit site (an Optional<T> setter receives null rather than Optional.empty() for default-slot inserts); the spec’s "parameter-type mismatch rejection" unit case is unimplemented (tryMutableBean accepts any single-arg setter on name match alone, the legacy convention’s de-facto behaviour). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R156 (ba4697f Phase A model: DataElement.Id, PerFieldOutcome, PkResolution, two new ChildField siblings; cbe4634 Phase B verb-aware carrier walk + DELETE projection; 61ce2c8 Phase C/D FieldBuilder rewire + MutationField DELETE admission lift + FieldRegistry.reclassify; 8d88bb5 Phase E/F per-field emitters + DELETE DML chain emission; fe676bf Phase G partial L1/L3; a869716 Phase G L5 sakila fixtures; 160f102 Phase G L6 execution-tier coverage; e08c439 Phase H user docs; 424bc42 doc sweep; b5209a9 In Review rework ; PkResolutionEmitterReachabilityTest, MutationDmlNodeIdClassificationTest admission cells, MutationDeletePayloadCarrierCase rejection rows): @mutation(typeName: DELETE) now supports payload-returning carriers on two element arms, closing the gap that left DELETE the only DML verb without a payload-carrier story (a prerequisite for composing with R12’s errors: channel). New DataElement.Id(name, wrapper) arm admits an ID / [ID!] carrier field that echoes encoded NodeIds of deleted rows; admitted only on DELETE per the permit-verb rule (PK-echo commits to the PK as the entire post-image; INSERT/UPDATE/UPSERT post-images are richer). The existing DataElement.Table arm narrows on DELETE: a new builder-internal PerFieldOutcome (five arms: PkRead, NonPkNullable, NonPkNonNullable, ServiceField, UnsupportedField) classifies every field on the element SDL type, and BuildContext.classifyDeleteTableProjection either rejects (any non-PK-non-null / @service / FK-traversing / unsupported leaf, with a diagnostic naming the offending field) or projects to the narrow model-facing PkResolution (two arms) carried on a new ChildField.SingleRecordTableFieldFromReturning permit. The companion ChildField.SingleRecordIdFieldFromReturning carries the CallSiteCompaction.NodeIdEncodeKeys for the Id arm; both are siblings of the existing SingleRecordTableField and load-bearing for distinct invariants (no follow-up SELECT after DELETE, the row is gone). BuildContext gains a verb-aware tryResolveSingleRecordCarrier(typeName, DmlKind) overload that delegates to the verbless walk and layers DELETE-admissibility on top, so the unconditional DELETE-rejection at FieldBuilder.java:2960-2965 disappears entirely; MutationDmlRecordField / MutationBulkDmlRecordField compact constructors lift the DELETE rejection symmetrically. FieldRegistry.reclassify is the named exception that lets the DELETE carrier path replace the verbless walk’s SingleRecordTableField registration (which assumed follow-up SELECT) with the DELETE-specific sibling. FetcherEmitter adds two methods: buildSingleRecordIdFromReturningFetcherValue reads PK column(s) off the source Record and runs them through the encoder; buildSingleRecordTableFromReturningFetcherValue synthesizes a PK-only Record via Tables.<TABLE>.newRecord() and copies PK columns from the RETURNING source (the same-Field<T>-instance round-trip is the load-bearing assumption documented on the emitter and in the spec’s §Runtime caveats). The producer-consumer pin is @LoadBearingClassifierCheck(key = "mutation-delete-carrier.pk-resolution-projection-clean") on classifyDeleteTableProjection plus matching @DependsOnClassifierCheck on the table-arm emitter. Tests: unit-tier DataElementIdInvariantTest (6 cases pinning compact-constructor wrapper invariants, singleton ID/ID! and [ID!]/[ID!]! admit, list-of-nullable and Connection wrappers reject); unit-tier PkResolutionEmitterReachabilityTest (4 cases: reflective scan of PkResolution arms against FetcherEmitter’s `HANDLED_BY_EMITTER allowlist, PerFieldOutcome rejection arms exist and do NOT leak into PkResolution, record-component symmetry across the two sealed roots, @LoadBearingClassifierCheck pin reflectively confirmed on classifyDeleteTableProjection); pipeline-tier MutationDmlNodeIdClassificationTest gains six R156 rows (single/bulk × implicit/explicit @nodeId admission cells over nodeidfixture’s composite-PK `Bar and single-PK Baz, plus wrong-encoder-table and no-@node-backed-input-table rejection paths); pipeline-tier MutationDeletePayloadCarrierCase parameterised in GraphitronSchemaBuilderTest covers the admission/rejection matrix (nullable non-PK admits with PkResolution.NonPkNullable projection; non-null non-PK rejects naming the field and pointing at DataElement.Id; INSERT/UPDATE/UPSERT + [ID!] reject via permit-verb rule; [ID] list-of-nullable rejects at the verbless walk; @service-resolved element field rejects); compile-tier graphitron-sakila-example adds DeletedFilmsIdPayload, DeletedFilmsTablePayload, DeletedFilmInfo types + deleteFilmsIdCarrier / deleteFilmsTableCarrier mutations; execution-tier DmlBulkMutationsExecutionTest adds two end-to-end tests against real PostgreSQL proving the encoded-NodeIds list comes back in input order and the per-field PK projection through the synthesized Record resolves. Deviation from spec (called out in spec body): user docs landed at docs/manual/reference/directives/mutation.adoc rather than docs/manual/reference/mutations.adoc because the existing layout puts directive references under directives/. Out of scope (called out, not regressed): affected-row count payload field (separate Backlog item; structurally different role permit); error-channel composition (R12’s upstream producer; ErrorChannelRole permit already composes with the new arms); RETURNING * or projection-aware RETURNING for arbitrary non-PK columns (rejected in §Alternatives, the user’s rule narrows projections to PK only); dialect-capability gating on DELETE-RETURNING (existing dialect-roadmap item covers RETURNING capability checks); soft warnings on silent-null non-PK nullable fields (classifier stays binary; documented behaviour instead). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R159 (6aababe admit + type-match + LSP arms; 8671b5b thread sourceSigil into DataChannel, route LSP through siteContext, drop back-compat constructors): root-value sigil $source on @field(name:) for carrier-payload sourcing. Authors now have an explicit, name-decoupled way to confirm the implicit binding between a @service-backed mutation’s reflected return and the SDL carrier-payload data field (the R158 contract). New FieldSourceSigil utility owns the sealed FieldNameRef = BareName | UpstreamRoot, ParseResult = Absent | Ok | UnknownSigil, and SiteContext = CarrierDataField | Other; the three canonical messages (unknown sigil, not-defined-here, type mismatch) live on the utility so classifier HardReject, LSP Diagnostics, and LSP FieldCompletions route through one source. BuildContext.classifyCarrierField interposes parseArgFieldNameRef before the forbidden-directive loop: UpstreamRoot lifts @field off the forbidden list for the iteration and threads sourceSigil = true into the emitted CarrierFieldRole.DataChannel(fieldName, element, sourceSigil); UnknownSigil HardRejects before the forbidden-directive loop fires (so the author sees "Unknown sigil" rather than "forbidden directive"). The type-match check runs at FieldBuilder.classifyMutationField’s `@service Resolved.Result arm (the colocation principle is preserved by the shared sourceSigilTypeMatches callable, with the bit-read replacing the SDL re-parse). LSP plumbing: CatalogBuilder.projectCarrierDataFields walks GraphitronSchema.fields() for ChildField.SingleRecord* permits to project Map<String, String> carrierDataFieldByType onto LspSchemaSnapshot.Built; Built.siteContext(typeName, fieldName) -> FieldSourceSigil.SiteContext is the one entry point consumers use, so broadening admit in a future item flips a single sealed return-value. FieldCompletions admits $source at carrier-data-field sites and stays silent everywhere else (including snapshot-uncertainty: no entry in the carrier projection → no suggestion). Diagnostics.validateFieldMember emits sourceSigilNotDefinedHereMessage at non-carrier sites whose parent’s TypeBackingShape is known; snapshot-uncertainty stays silent (defers to the build). LspSchemaSnapshot.Built.Current / Built.Previous collapse to single canonical three-arg constructors; the two-arg back-compat overloads were a shim with no external producer to protect. Tests: pipeline-tier FieldSourceSigilPipelineTest covers admit, model-shape regression (with/without @field byte-identical), type-mismatch reject, unknown-sigil reject (parse-time arm fires before forbidden-directive HardReject), bare-name regression, non-carrier-site regression (today’s accessor-mismatch unchanged) ; each rejection case also asserts validator-surface ValidationReport.errors() via the same fixture; LSP-tier FieldCompletionsTest + DiagnosticsTest each gain three R159 cases (admitted / non-carrier / snapshot-uncertainty). Deviation from spec (called out in spec body): type-match site is FieldBuilder.classifyMutationField, not classifyCarrierField, because the producer’s MethodRef is bound at consumer-site classification not at the carrier walk; FieldSourceSigil.sourceSigilTypeMatches uses exact equality for both DataElement.Table and DataElement.Record today (spec called for assignability on the @record arm; the implementer’s note acknowledges "future items may relax when a forcing function appears"). Out of scope (called out, not regressed): admission at sites other than the carrier-payload data field (future broadening flips sourceSigilDefinedAt); $errors / $context / other sigils; dotted paths in @field(name:); multi-step path-expression grammar; DML-producer carrier walk migration to a $source model (R75 / R141 keep PK-keyed-map); execution-tier coverage of the OpprettRegelverksamlingPayload-shaped end-to-end gated on R158 landing the consumer-side fetcher. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R139 (dev-pipeline to LSP schema-snapshot side-channel; first client unknown-directive validator): the bundled directives.graphqls was the LSP’s entire view of "what directives exist", so any user-authored directive (@key(fields:), @requiresAuthentication, @auth(role:), etc.) drew a Diagnostics warning per use as if it were a typo. Fix: a second volatile ref on Workspace (alongside catalog) carrying a projection of the parsed user schema, populated from the same parse the catalog already runs. New sealed LspSchemaSnapshot (Unavailable | Built.{Current,Previous}) over two orthogonal axes ; availability (built yet?) and freshness (latest successful parse?) ; lifts the Diagnostics.validateClassName-style "pre-build silence" gate from a single arm into a typed sub-hierarchy. Built carries List<DirectiveShape> with sibling InputValueShape + sealed TypeShape (Named | List) so phase-2 arg-validation consumers discriminate list-vs-named without re-parsing rendered SDL. Producer is CatalogBuilder.buildSnapshot(TypeDefinitionRegistry) (returns Built.Current only; failures throw upstream in GraphQLRewriteGenerator), pinned with @LoadBearingClassifierCheck on the keys snapshot-built-implies-clean-parse and snapshot-directive-roundtrip-faithful. Atomic-pair swap path is Workspace.setBuildOutput(...) (absorbing the spec’s separately drafted setCatalogAndSnapshot and setCatalog-on-classpath setters, since the validator-report side-channel that landed on top of R139 wants the same atomic swap discipline); failure path is Workspace.demoteSnapshot() which transitions Built.Current → Built.Previous and is no-op on Unavailable / Previous. Resolution goes through sealed DirectiveResolution.resolve(LspVocabulary, LspSchemaSnapshot, String) returning Bundled | User | Unknown, encoding bundled-shadows-snapshot precedence once so consumers never re-check it inline; Workspace.resolveDirective(String) wraps the static entrypoint for request callbacks that already hold a Workspace. First client: the unknown-directive arm in Diagnostics.compute switches exhaustively on the snapshot variant for the freshness-aware silence policy ; warns only under Built.Current + Unknown, silences Unavailable (pre-build), Built.Previous (stale after parse failure), and any User / Bundled resolution. SPEC_BUILTIN_DIRECTIVES (skip, include, deprecated, specifiedBy, oneOf) keeps its short-circuit because graphql-java ships them implicitly. DevMojo.regenerate / rebuildCatalog both call setBuildOutput(...) on success and demoteSnapshot() + markAllForRecalculation() on the parse-failure catch. Tests: unit-tier LspSchemaSnapshotTest (case-sensitive lookup across both Current / Previous, unmodifiable defensive copy at construction) and CatalogBuilderSnapshotTest (directive round-trip, list/non-null sealed projection, no producer-side bundled-name filter, description round-trip); pipeline-tier DiagnosticsTest grows unknownDirectiveSilencedByUnavailableSnapshot, unknownDirectiveSilencedByStaleSnapshot, userDeclaredDirectiveSilencedBySnapshot, userDeclaredDirectiveShadowedByBundledStillValidates, plus the existing unknownDirectiveProducesWarning updated to pass an explicit Built.Current(List.of(), Map.of()); compilation-tier sakila fixture declares directive @auth(role: String!) on FIELD_DEFINITION and applies it on Query.customers as the input-contract regression guard. Out of scope (called out, not regressed): hover / arg-completion / arg validation against user directives (later items; phase-2 arm validateUnknownArgsAgainstSnapshot / validateRequiredArgsAgainstSnapshot rides on the same plumbing); a declaredTypeNames set on Built (R157 widened the permits with typesByName for the analogous record/POJO use case); widening LoadBearingGuaranteeAuditTest across the graphitron / graphitron-lsp module boundary (consumer-side @DependsOnClassifierCheck markers on Diagnostics.compute and Workspace.resolveDirective are find-usages-only by design); a shadow-warning for user directives that redeclare bundled names; server-mode LSP without the dev mojo. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R157 (6c33331 implement LSP @field(name:) coverage for @record-bound types; 6b9ea86 sealed-split JooqRecordBacking, drop back-compat shims, rename validator): @field(name: "X") autocomplete / diagnostics / hover only fired on jOOQ-table-bound parents; under @record-declared Java records, POJOs, and standalone jOOQ records the three consumer sites (FieldCompletions.generate, Diagnostics.validateCatalogColumn, Hovers.columnHover) silently returned empty because they parsed the enclosing type’s @table directive off the SDL AST and had no path to consult the classifier’s record/POJO knowledge. Fix: shift the LSP off SDL re-sniffing onto the classifier’s lifted model. New sealed TypeBackingShape (RecordBacking | PojoBacking | JooqRecordBacking{WithTable|Standalone} | TableBacking | NoBacking{Root|UnbackedResult|UnclassifiedInterface}) projects every GraphitronType permit to the LSP-visible backing shape; the projector lives in CatalogBuilder.projectType as an exhaustive sealed switch so a future GraphitronType variant trips a compile error at the projection site. LspSchemaSnapshot.Built.{Current,Previous} broaden to carry Map<String, TypeBackingShape> typesByName alongside the R139 directives list; GraphQLRewriteGenerator.buildOutput wires the three-arg buildSnapshot(registry, schema, catalog) form. ClasspathScanner reads the JVM Record attribute via java.lang.classfile.attribute.RecordAttribute so CompletionData.ExternalReference carries List<RecordComponent> per scanned class; the projector consumes these for RecordBacking and the bean-accessor filter (get<X> / is<X> no-arg public method) lives in CatalogBuilder.beanAccessorSlot for PojoBacking. The three consumer sites pattern-dispatch on the sealed permit (Diagnostics.validateCatalogColumn renamed to validateFieldMember, since it dispatches across four backing shapes plus NoBacking now); TypeContext.tableNameOf is gone from the three sites but stays for @nodeId(typeName:)’s metadata projection (R152 owns that migration). The `@LoadBearingClassifierCheck("java-record-type-backs-record-class") annotation on CatalogBuilder.buildSnapshot pins the assumption the three LSP-side @DependsOnClassifierCheck consumers make about JavaRecord{Input,}Type backing real Java record classes; the audit-test scope is graphitron-module-only by design (the LSP-side consumers wear the annotation for find-usages navigation and reviewer-signal purposes per rewrite-design-principles.adoc § "Classifier guarantees shape emitter assumptions"’s producer-without-consumer allowance). Tests: primary-tier `R157PipelineTest parses a realistic .graphqls, runs GraphitronSchemaBuilder for real, scans the LSP module’s target/test-classes for fixture classes (R157FilmRecord, R157FilmPojo), builds the full snapshot through CatalogBuilder, and drives FieldCompletions / Diagnostics end-to-end; unit-tier CatalogBuilderSnapshotTest adds per-variant projection cases (TableType, TableInterfaceType, JavaRecordType, PojoResultType.Backed, JooqTableRecordType, JooqRecordType-standalone, RootType, plain InterfaceType, PojoResultType.NoBacking); ClasspathScannerTest covers the Record-attribute read plus the plain-class empty case; FieldCompletionsTest, DiagnosticsTest, HoversTest adopt the snapshot-keyed dispatch and add positive cases per TypeBackingShape arm (RecordBacking → component list, PojoBacking → bean accessors, JooqRecordBacking.WithTable → column-on-table path, Standalone → silence, TableBacking → unchanged column-on-table path, NoBacking → silence). Out of scope (called out, not regressed): @enum(enum: {className:}) types (don’t carry @field(name:)); @reference(key:) on non-table backings (FKs are intrinsically a jOOQ-table concept; the directive stays on the existing TypeContext.tableNameOf path); union types as @field parents (meaningless directly; flows to NoBacking.UnbackedResult); migrating @nodeId(typeName:)’s metadata projection onto `typesByName (R152 owns the table-of-other-type scoping bug); per-component nullability / Jakarta-constraint surfacing on MemberSlot (R12-adjacent). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R153 (555fa0f + 32c7ebe): Attach explicit TextEdit range to every LSP CompletionItem. Hoists the cursor walk + replace-range computation into LspVocabulary.locateAt (returns Optional<CursorLocation> carrying the schema coordinate plus the tree-sitter leaf node) and CompletionContext (carries the LSP Range derived by slicing the leaf: string_value strips one or three bytes per side, discriminated by content; enum_value / bare name use the full span); coordinateAt collapses to a thin wrapper preserving the Hovers caller. All six string-value providers (ClassName, Method, Table, Field, Reference, ScalarType) plus NodeTypeCompletions and ArgNameCompletions ship setTextEdit, so eglot’s graphql-mode syntax table (which excludes . as a symbol constituent) no longer concatenates the prefix with the candidate on dotted FQNs like com.example.FilmServ| becoming com.example.com.example.FilmService. Empty literals and block strings flow through the same slicing rule ("" and """""" collapse to zero-width at the inner cursor); ArgNameCompletions also fires on the arg-key side of an already-filled arg for partial-identifier completion. Coverage: LspVocabularyLocateAtTest (9 cases pinning node-kind dispatch plus empty-literal / block-string / empty-object-value corners) and CompletionTextEditTest (12 cases, one regression pin per provider plus cursor-on-quote / block-string / empty-literal / zero-width-on-whitespace). Out of scope (called out, not regressed): filterText for partial-match scoring and insertText snippet syntax; the user-directive arg-name path does not yet fire on the arg-key side of an already-filled arg (the bundled arm does; incidental asymmetry, would warrant its own roadmap item if a gap surfaces). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R155 (0bd77de boundary box + invariant pin; fc55fa6 emitter-tier pin): graphitron:dev blew up with IllegalArgumentException: couldn't make a guess for int on consumer schemas whose input beans carried a Java-primitive field (record int n, JavaBean void setActive(boolean), …) because java.lang.reflect.Type.getTypeName() returns the unboxed primitive literal and that string flowed unchanged through FieldBinding.javaElementTypeName into the two ClassName.bestGuess sites in InputBeanInstantiationEmitter (fieldLocalType, directExpr). Fix: normalise at the resolver boundary. New InputBeanResolver.boxPrimitive(String) maps each of the 8 primitive literals (int/long/boolean/double/float/short/byte/char) to its wrapper FQN (java.lang.Integer/…) and passes everything else through; peelJavaListSet calls it on the scalar return so FieldBinding.javaElementTypeName becomes invariantly a real class name, never a Java primitive literal. The list branches deliberately don’t box: Java disallows List<int>, so the generic argument is always already a reference type. Javadoc on the helper and a sentence on FieldBinding.javaElementTypeName pin the contract as prose; no @LoadBearingClassifierCheck annotations owed (this is representation-normalisation at the resolver, not a new classifier branch with downstream shape obligations). Tests: unit-tier InputBeanResolverBoxPrimitiveTest pins the full 8-arm primitive→wrapper mapping plus class-name pass-through plus int[] fallthrough; pipeline-tier GraphitronSchemaBuilderTest gains two cases (SERVICE_MUTATION_FIELD_INPUT_BEAN_PRIMITIVE_RECORD, SERVICE_MUTATION_FIELD_INPUT_JAVABEAN_PRIMITIVE_BOOLEAN) asserting FieldBinding.javaElementTypeName == "java.lang.Integer" / "java.lang.Boolean" on the record-component and JavaBean-setter paths respectively; TypeFetcherGeneratorTest gains two mirror cases pinning that InputBeanInstantiationEmitter.buildSingularHelper no longer throws on a boxed primitive FieldBinding and emits the wrapper-typed local and cast. Out of scope (called out, not regressed): generalising ClassName.bestGuess in graphitron-javapoet to accept primitives (javapoet treats primitives via TypeName.INT, a wider refactor of no immediate value); tightening the String-typed representation of FieldBinding.javaElementTypeName / EnumValueOf.enumClassName to a typed ClassName or sealed JavaTypeRef so the "real class name, never a primitive literal" invariant is a type fact rather than prose (separate Backlog item). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R144 (db40644 ship cardinality safety default; a142f78 cover @value + @condition mutual-exclusion rejection; 11bd6ee drop unreachable admissibleCount == 0 branches): Flip the polarity of mutation-input safety. Every input field on a DELETE / UPDATE @table input is a WHERE filter by default; the classifier enforces that the contributed filter columns cover the input @table’s primary key. `@mutation(multiRow: true) opts out of the PK-coverage check, naming the hazard rather than the mechanism. @value on input fields marks UPDATE assignment columns; rejected on DELETE / INSERT / UPSERT and mutually exclusive with @condition on the same field. @lookupKey on INPUT_FIELD_DEFINITION is retired with a migration diagnostic surfaced at both per-field and per-arg classification sites (the ARGUMENT_DEFINITION use for Query-side LookupTableField / SplitLookupTableField is untouched). UPSERT is refused upstream at MutationInputResolver with a Rejection.deferred keyed to R145 (mutation-cardinality-safety-upsert); R141’s compact-constructor UPSERT rejection on MutationBulkDmlRecordField becomes a redundant type-system backstop during the R144-shipped-but-R145-not-yet window. Carrier change: TableInputArg.of now takes DmlKind kind and the @value-marked field-name set; partitions lookupKeyFields / setFields per verb (UPDATE: complement vs intersection on the @value set; DELETE / INSERT: setFields empty by classifier guarantee). EnumMappingResolver.buildLookupBindings drops the DIR_LOOKUP_KEY gate and walks every admissible input field minus a caller-supplied exclude set (the UPDATE @value names). Audit producers: two new @LoadBearingClassifierCheck keys on MutationInputResolver.resolveInput (mutation-input.where-columns-cover-pk, mutation-input.update-set-fields-equal-value-marked) with @DependsOnClassifierCheck consumers on the eleven tia.setFields() walk sites in TypeFetcherGenerator’s UPDATE arms and on the `MutationBulkDmlRecordField construction site (so any future refactor that branches the bulk path around resolveInput surfaces as an orphaned consumer in LoadBearingGuaranteeAuditTest). Migration: sakila example schema migrated (upsertFilm / upsertFilms / upsertFilmPayload retired; FilmUpdateInput.title / .description carry @value; new FilmReleaseYearDeleteInput + deleteFilmsByReleaseYear multiRow fixture); classifier truth-table fixtures retyped to the new diagnostics; UPSERT execution tests in DmlBulkMutationsExecutionTest, GraphQLQueryTest, and SingleRecordCarrierDmlTest @Disabled with R145 reference. Tests: pipeline-tier R144_* rows on GraphitronSchemaBuilderTest.MutationDmlCase (PK-coverage admission/rejection, multiRow admission, @value-on-DELETE rejection, multiRow-on-INSERT rejection, @value`@condition+` mutual-exclusion) plus UPDATE_NO_VALUE_FIELDS_REJECTED, UPDATE_EVERY_FIELD_VALUE_MARKED_REJECTED, UPDATE_PARTIAL_COMPOSITE_PK_REJECTED, DELETE_PARTIAL_COMPOSITE_PK_REJECTED, UPDATE_TIA_PARTITIONS_FIELDS_INTO_LOOKUP_AND_SET; execution-tier proof DmlBulkMutationsExecutionTest.deleteFilmsByReleaseYear_multiRowBroadcastsAcrossInputCardinality asserts |affected rows| == 3 while |input rows| == 1 against a release-year-keyed broadcast. The Spec’s "empty input + multiRow`" and "DELETE with zero admissible carriers" rejection bullets shipped as unreachable defensive checks (graphql-java rejects empty input types at parse with `"InputObjectType ... must define one or more fields", and the per-field loop in resolveInput rejects every non-admissible field shape before the admissible-count check); both branches were removed per the project’s "no error handling for scenarios that can’t happen" rule with a Javadoc note on resolveInput recording the parser-level guarantee. Docs: new docs/manual/reference/directives/value.adoc covers the @value surface, per-verb validity rules, and the cardinality-safety interaction with multiRow; DirectiveDocCoverageTest green. Out of scope (filed as follow-ups): R145 (mutation-cardinality-safety-upsert) re-admits UPSERT with a designed cardinality story; R146 (mutation-cardinality-safety-unique-index) lifts the PK-only conservative cut to PK-or-unique-index coverage. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R43 commit 5 (ChildField.RecordTableMethodField DTO-parent emit; lift to IMPLEMENTED_LEAVES; R43 In Progress → In Review): SplitRowsMethodEmitter.buildForRecordTableMethod emits the DataLoader rows-method for the new variant: parent VALUES table over the FK source-side columns + the developer’s static @tableMethod call substituted for the terminal Tables.<X>.as("alias") declaration + flat SELECT with JOIN parentInput ON terminal.<targetSide> = parentInput.<sourceSide> + scatter via scatterByIdx (list cardinality) or scatterSingleByIdx (single cardinality / LOAD_MANY). The new RowsMethodBody.SqlRecordTableMethod sealed permit gives the body the same framing as the existing SqlRecordTable* siblings (RowsMethodSkeleton emits the empty-input gate + DSL local; the permit’s content references both). TypeFetcherGenerator.buildRecordBasedDataFetcher’s generic constraint loosens from `<T extends TableTargetField & BatchKeyField> to <T extends GraphitronField & BatchKeyField> (taking returnType: ReturnTypeRef.TableBoundReturnType as a parameter) so the existing record-parent DataFetcher emit is shared across the three variants without an interface widening; RecordTableField / RecordLookupTableField call sites thread their returnType() explicitly. The scatterByIdx helper-emission gate gains a RecordTableMethodField-with-list-cardinality-non-single arm; the existing BatchKeyField.emitsSingleRecordPerKey gate already covers scatterSingleByIdx. Dispatch lift: RecordTableMethodField moves from STUBBED_VARIANTS to IMPLEMENTED_LEAVES; the deferred-slug entry retires; generateTypeSpec’s switch arm flips from `builder.addMethod(stub(f)) to the paired buildRecordBasedDataFetcher + SplitRowsMethodEmitter.buildForRecordTableMethod calls (mirroring RecordTableField). The variant overrides emitsSingleRecordPerKey() to fold single-cardinality fields onto the single-record-per-key arm, same shape as RecordTableField’s override. Path shape coverage: single-hop `JoinStep.FkJoin is the shipped emit form (the common case, and the only one exercised by the planned pipeline + execution coverage); multi-hop FK paths and JoinStep.ConditionJoin terminals surface a runtime UnsupportedOperationException with a labelled message, mirroring the table-parent TableMethodField commit-3 emit. Pipeline tests: TableMethodFieldPipelineTest.dtoParentFkAutoDerive_emitsDataLoaderFetcherAndRowsMethod pins the FilmRecord-backed @record parent + auto-FK-derive + explicit @reference path shape (the generated FilmDetailsFetchers.language DataFetcher signature is CompletableFuture<DataFetcherResult<Record>> wiring a DataLoader / rowsLanguage call; the rows method body invokes TestTableMethodStub.getLanguage, builds parentInput, and joins on LANGUAGE_ID). RowsMethodSkeletonTest.rowsMethodBody_sealedSwitchIsExhaustive count increments to six (the new permit), and UnifiedEmissionPinsTest.rowsMethodEmitter_unifiedSkeleton count increments to six (the new entry method emits two RowsMethodSkeleton.build calls: one for the emit-able single-hop FK arm, one for the multi-hop / ConditionJoin / empty-path runtime stub). Execution test: GraphQLQueryTest.filmById_detailsForMethod_languageViaTableMethod_routesThroughRecordTableMethodFieldDtoParentEmit exercises the end-to-end DTO-parent path against rewrite_test. Sakila fixture additions: a new SDL type FilmDetailsForMethod @record(record: FilmRecord) with filmId, languageId (declared so the parent SELECT projects film.language_id), and languageViaTableMethod: Language @tableMethod(...) @reference(path: [{key: "film_language_id_fkey"}]); a new Film.detailsForMethod: FilmDetailsForMethod field that’s a ConstructorField passthrough so the parent Film row record flows through to FilmDetailsForMethod’s source-record slot. The query { filmById(film_id: ["1", "2"]) { filmId detailsForMethod { filmId languageId languageViaTableMethod { languageId name } } } } returns the seeded language_id=1 / name="English" for both films, confirming the developer’s tableMethodLanguage() table is correctly joined against the lifted FK keys. Out of scope (called out, not regressed): multi-hop FK path emit and ConditionJoin terminal emit for RecordTableMethodField (the runtime stubs are loud, not silent); execution-tier coverage for the @sourceRow arm (a separate fixture with a hand-written lifter is a candidate follow-up; the classifier branch is already covered by RecordTableMethodFieldCase). R43 moves In Progress → In Review with this commit. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25 (1683 graphitron tests + sakila-example compile + execute tiers all passing).

  • R43 commit 4 (ChildField.RecordTableMethodField variant added, emit stubbed): New sealed-permit on ChildField covers child @tableMethod fields on @record (non-table) parents ; the DTO-parent sibling of ChildField.TableMethodField. The record carries parentTypeName, name, location, ReturnTypeRef.TableBoundReturnType returnType, List<JoinStep> joinPath, MethodRef method, SourceKey sourceKey, LoaderRegistration loaderRegistration, Optional<ErrorChannel> errorChannel and implements ChildField, MethodBackedField, BatchKeyField, WithErrorChannel. It wears the shared @DependsOnClassifierCheck("tablemethod-resolver-return-is-table-bound") audit annotation (the resolver-side rejection of non-table returns underwrites the narrowed component type the same way it does for TableMethodField / QueryTableMethodTableField). Classifier: FieldBuilder.classifyChildFieldOnResultType grew a new @tableMethod branch placed before the @sourceRow branch, so both directives can coexist on the same field (their roles are complementary: @sourceRow provides the batch-key lifter; @tableMethod provides the developer’s static jOOQ table method). Two admit arms: (a) JooqTableRecordType parent + unique catalog FK between parent’s table and @tableMethod return-type table ; auto-derives the SourceKey via the existing deriveFkRecordParentSource helper, same Wrap.Row + Reader.ColumnRead shape RecordTableField’s FK arm produces; (b) free-form DTO parent (PojoResultType` / JavaRecordType) + @sourceRow(className:, method:) ; delegates to SourceRowDirectiveResolver for the lifter-derived SourceKey (Wrap.Record + Reader.SourceRowsCall). Both arms compose with @reference(path:) for explicit FK chains. The same last-hop-target check from the table-parent branch applies. A free-form DTO without @sourceRow and without FK metadata produces a structured UnclassifiedField AUTHOR_ERROR enumerating the three lift options (typed jOOQ TableRecord backing, @sourceRow, or a typed accessor). Dispatch / emit: STUBBED_VARIANTS gains an entry keyed on RecordTableMethodField.class with plan slug tablemethod-child-table-bound (commit 5 will lift it to IMPLEMENTED_LEAVES); generateTypeSpec’s child switch arm routes `RecordTableMethodField through stub(f) so schemas exercising the variant fail at validate-time with the standard deferred message rather than crashing at request time. Validator: GraphitronSchemaValidator gained a new switch arm + validateRecordTableMethodField helper applying the existing validateReferencePath + validateCardinality checks (mirror of validateTableMethodField for the table-parent sibling). Carrier-plumbing: MappingsConstantNameDedup.withResolvedChannel rebuilds the new variant via its existing WithErrorChannel sealed switch. Tests: new RecordTableMethodFieldCase enum in GraphitronSchemaBuilderTest pins three classifier shapes ; JOOQ_TABLE_RECORD_PARENT_AUTO_FK (FilmRecord parent + getInventory auto-FK to Inventory), JOOQ_TABLE_RECORD_PARENT_EXPLICIT_REFERENCE (FilmRecord parent + getLanguage with explicit @reference(path: [{key: "film_language_id_fkey"}])), and FREE_FORM_PARENT_NO_SOURCEROW_REJECTED (DummyRecord parent + getInventory without @sourceRow or FK metadata → rejection naming the three lift options). TestTableMethodStub gains getInventory() returning Inventory.class for the new tests. VariantCoverageTest confirms the new sealed leaf has classification coverage; the existing partition tests (GeneratorCoverageTest.everyGraphitronFieldLeafHasAKnownDispatchStatus and notImplementedReasonsContainsOnlyConcreteSealedLeaves) stay green because the new class is keyed in STUBBED_VARIANTS rather than IMPLEMENTED_LEAVES. Out of scope (kept under R43 commit 5): DTO-parent emit reusing RecordTableField’s DataLoader-keyed batch pattern with the developer’s static method substituted; pipeline-tier fetcher emission tests; execution-tier coverage against `rewrite_test; move RecordTableMethodField from STUBBED_VARIANTS to IMPLEMENTED_LEAVES. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25 (1673 graphitron tests passing).

  • R43 sub-commit (FK-projection injection for child @tableMethod on table-bound parents): Closes the execution-tier gap commit 3 left open. The child @tableMethod fetcher reads parentRecord.get(DSL.name("<sourceSqlName>"), …) for parent-row correlation; without injecting the FK source-side column into the parent SELECT, the read throws IllegalArgumentException: Field "<col>" is not contained in row type ("<schema>"."<parentTable>"."<pk>") whenever the user’s SDL selection omits the FK column. Fix: TypeClassGenerator.collectSourceKeyColumns generalises to collectRequiredProjectionColumns and gains a ChildField.TableMethodField arm that extracts the single-hop JoinStep.FkJoin’s `sourceSideColumns() and threads them through the existing requiredProjectionColumns pipeline ; same if (!fields.contains(table.$L)) fields.add(table.$L) idempotent-append idiom Split* fields already use for their SourceKey columns. Only single-hop FkJoin shapes contribute: multi-hop and ConditionJoin paths surface a runtime UnsupportedOperationException in buildChildTableMethodFetcher anyway, so projecting their first hop would synthesise dead columns. NestingField recursion preserved so nested @tableMethod fields under a non-table-bound nested type get their FK columns into the outer table-class’s $fields. Pipeline tests: TableMethodFieldPipelineTest gains singleFkAutoInferred_parentDollarFieldsProjectsFkSourceColumn (auto-FK Inventory→Film pins FILM_ID on Inventory.$fields) and explicitReferencePathSingleHopFk_parentDollarFieldsProjectsFkSourceColumn (explicit @reference(path: [{key: "film_language_id_fkey"}]) pins LANGUAGE_ID on Film.$fields); both use the shared TypeSpecAssertions.appendsRequiredColumn helper that already pins the same idiom for Split* fields. Execution tests: GraphQLQueryTest gains inventoryById_filmViaTableMethod_correlatesParentRowViaInjectedFkProjection (three Inventory rows each correlating to their matching Film by inventory.film_id; asserts film.filmId and film.title per row) and filmById_languageViaTableMethod_correlatesParentRowViaExplicitReferencePathFk (two Film rows each correlating to Language via film.language_id; asserts language.languageId == 1 and stripped language.name == "English"). Both queries deliberately omit the FK column from their SDL selection so the projection-injection path is exercised end-to-end. Out of scope (kept under remaining R43 commits): the new ChildField.RecordTableMethodField variant for DTO-parent batching (commit 4); DTO-parent emit + execution coverage (commit 5). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R43 commit 3 (child table-bound-parent lift: ChildField.TableMethodField moves to IMPLEMENTED_LEAVES): New TypeFetcherGenerator.buildChildTableMethodFetcher emits a per-row fetcher modelled on the root-site buildQueryTableMethodFetcher: declare parentRecord = (Record) env.getSource(), call the developer’s static @tableMethod to derive the target table local, declare dsl, build a parent-correlation Condition from the resolved JoinStep chain, then SELECT the typed $fields projection from the developer-returned table with the correlation as WHERE. The new helper buildTableMethodParentCorrelation walks each JoinSlot of the (single) FkJoin hop and emits table.<targetSide>.eq(parentRecord.get(DSL.name("<sourceSqlName>"), <columnClass>.class)), threading the typed parentRecord.get(name, Class) overload so the resulting Condition type-checks against Field<T>.eq(T). Composite FKs AND across slots; an empty-slots fallback emits DSL.noCondition() so the catalog-unavailable case fails loudly at runtime. Path shape coverage: single-hop JoinStep.FkJoin is the shipped emit form (the common case, and the one exercised by R43’s planned pipeline + execution coverage). Multi-hop FK paths and JoinStep.ConditionJoin terminals are still accepted by the classifier (GraphitronSchemaBuilderTest.TableMethodFieldCase.LIST_RETURN / CONNECTION_RETURN / WITH_CONDITION_PATH) but the emitter surfaces a runtime UnsupportedOperationException with the shape label ("empty joinPath" / "multi-hop join path" / "ConditionJoin path") so the gap is loud rather than silent. Three @DependsOnClassifierCheck annotations pin the producer/consumer contracts (tablemethod-resolver-return-is-table-bound for the narrowed return type, service-catalog-strict-tablemethod-return for the no-downcast emit, service-catalog-tablemethod-must-be-static for the static-call shape) plus fk-join.slots-oriented-source-and-target on buildTableMethodParentCorrelation. Dispatch: IMPLEMENTED_LEAVES gains ChildField.TableMethodField.class; STUBBED_VARIANTS loses its entry (the tablemethod-child-table-bound plan-slug binding retires); generateTypeSpec’s child switch arm flips from `builder.addMethod(stub(f)) to builder.addMethod(buildChildTableMethodFetcher(ctx, f, outputPackage)). Pipeline tests: new TableMethodFieldPipelineTest two cases: singleFkAutoInferred_emitsFetcherMethod (Inventory.film with single-FK auto-inference) and explicitReferencePathSingleHopFk_emitsFetcherMethod (Film.language with @reference(path: [{key: "film_language_id_fkey"}])); both assert the generated FilmFetchers / InventoryFetchers contain a fetcher method under the field name with the standard (DataFetchingEnvironment) signature, the correct DataFetcherResult<Record> return type, and a body that invokes the developer-authored static method. TableMethodFieldValidationTest’s three `stubbedError cases (NO_PATH, WITH_FK_PATH, WITH_CONDITION_ONLY) flip to assert empty errors ; TableMethodField is no longer in STUBBED_VARIANTS, so GraphitronSchemaValidator.validateVariantIsImplemented is silent for these fixtures. Compile-tier coverage: sakila-example schema gains Inventory.filmViaTableMethod: Film @tableMethod(...) (auto-FK single-hop) and Film.languageViaTableMethod: Language @tableMethod(...) @reference(path: [{key: "film_language_id_fkey"}]) (explicit single-hop path); SampleQueryService adds tableMethodFilm() / tableMethodLanguage() returning Tables.FILM / Tables.LANGUAGE directly; the graphitron-sakila-example compile step type-checks the generated fetcher bodies against the real jOOQ classes (FilmFetchers.languageViaTableMethod and InventoryFetchers.filmViaTableMethod). Out of scope (deferred to a follow-up): runtime execution-tier coverage requires the parent fetcher to project the FK source column (e.g. inventory.film_id) when the child @tableMethod field is in the selection set; today the parent’s $fields walks only user-requested SDL fields and parentRecord.get(DSL.name("film_id"), …) fails with IllegalArgumentException: Field "film_id" is not contained in row type ("public"."inventory"."inventory_id"). Mechanism for FK-column injection (analogous to the projection synthesis that NodeIdReference / CompositeColumnReference already do at classify time) is a separable concern from the lift itself; commit 3 ships the emit + dispatch lift, and a follow-up R43 sub-commit will land FK-projection injection so the sakila fixture’s runtime path comes online. Out of scope (kept under remaining R43 commits): multi-hop FK path emit; ConditionJoin emit; the new ChildField.RecordTableMethodField variant for DTO-parent batching (commit 4); DTO-parent emit (commit 5). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25 (1640+ graphitron tests + sakila-example compile + execute tiers all passing).

  • R43 commit 2 (path resolution + last-hop-target validation for @tableMethod at child sites): FieldBuilder.classifyChildFieldOnTableType’s `@tableMethod arm reorders the resolver and parsePath calls so the return-type table is known before path resolution: tableMethodResolver.resolve runs first, then ctx.parsePath(fieldDef, name, tableType.table().tableName(), tb.returnType().table().tableName(), buildWrapper(fieldDef).isList()) runs with the target table populated, exercising the existing auto-FK inference branch in parsePath (findForeignKeysBetweenTables with directiveAbsent=true). The classifier adds a last-hop-target check: if the resolved path is non-empty and its last hop is a JoinStep.FkJoin, the hop’s targetTable().tableName() must equal the return-type’s table name (case-insensitive), else surface "@tableMethod @reference path: last hop lands on '<X>' but @tableMethod's return type is bound to table '<Y>'". JoinStep.ConditionJoin last hops are exempted from the structural check by design ; the condition method’s signature is the implicit contract there. Three accepted shapes (matching @reference semantics): (a) no @reference + exactly one FK between parent and return-type tables → single-hop FkJoin auto-inferred; (b) explicit @reference(path: [{key: "..."}, ...]) → walks each hop, last hop must land on return-type table; (c) @reference(path: [{condition: {className, method}}]) → ConditionJoin terminal. Three rejection shapes: ambiguous FK (multiple FKs between parent and target, no @reference) surfaces the existing fkCountMessage "multiple foreign keys found between tables …" with directive-absent guidance; missing FK + no @reference surfaces fkCountMessage’s zero-FK arm; last-hop-target mismatch surfaces the new structural rejection. Test fixtures updated for the new behaviour: `GraphitronSchemaBuilderTest.TableMethodFieldCase.SINGLE_RETURN / LIST_RETURN / CONNECTION_RETURN / TABLE_METHOD_FIELD_CONTEXT_ARGS add explicit @reference(path: …) (Film→Language had two FKs and would now reject as ambiguous; Film→Actor has no direct FK and would now reject as missing). New pipeline tests: TableMethodFieldCase.WITH_AUTO_FK_INFERENCE (Inventory→Film single-FK auto-infers a single-hop FkJoin landing on film); TableMethodFieldCase.WITH_CONDITION_PATH (@reference(path:[{condition:…}]) resolves to ConditionJoin). New rejection tests in UnclassifiedFieldCase: TABLEMETHOD_CHILD_AMBIGUOUS_FK_REJECTED (Film→Language with no @reference); TABLEMETHOD_CHILD_MISSING_FK_REJECTED (Film→Actor with no @reference); TABLEMETHOD_CHILD_LAST_HOP_MISMATCH_REJECTED (@reference(path:[{key:"film_language_id_fkey"}]) on a field declared Actor → last-hop-lands-on-language rejection). No emit change: TypeFetcherGenerator.STUBBED_VARIANTS still maps ChildField.TableMethodField.class to the deferred slug; the dispatch in generateChildFetcher still routes to stub(f). The lift to IMPLEMENTED_LEAVES is R43 commit 3. Out of scope: emit (commit 3); the new ChildField.RecordTableMethodField variant for DTO-parent batching (commit 4); DTO-parent emit (commit 5). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R43 commit 1 (directive flattening + method-signature rewrite at the root site): Retired BuildContext.ARG_TABLE_METHOD_REF; flattened the @tableMethod directive in directives.graphqls from (tableMethodReference: ExternalCodeReference!, contextArguments:) to (className: String!, method: String!, argMapping: String, contextArguments: [String!]), mirroring @sourceRow. TableMethodDirectiveResolver.resolve parses the flat args inline (sibling to SourceRowDirectiveResolver); parseExternalRef stays as-is for @service / @externalField. Method-signature contract: developer’s @tableMethod static method now receives only GraphQL field arguments and contextArguments: values ; no Table parameter. ServiceCatalog.reflectTableMethod gains a new TableSlotPolicy { REQUIRED, FORBIDDEN } parameter so the three call sites (TableMethodDirectiveResolver, two @condition paths in ConditionResolver, two @condition paths in BuildContext.parseCondition*) pick their semantics: @tableMethod passes FORBIDDEN (reject any Table<?> parameter; no foundTable requirement), @condition callers pass REQUIRED (keep the original ParamSource.Table slot + foundTable invariant + the reserved-Table-slot argMapping typo guard, factored as the new checkConditionOverrideTargets helper). TypeFetcherGenerator.buildQueryTableMethodFetcher now passes null for tableExpression to ArgCallEmitter.buildMethodBackedCallArgs and the emitted call drops the leading Tables.<NAME> argument; the body shape stays var table = ClassName.method(<args>) with <args> now sourced exclusively from ParamSource.Arg / ParamSource.Context slots. The flat form drops the deprecated name: alias on @tableMethod (the existing @sourceRow precedent already shipped without it). LSP canonical overlay (LspVocabulary.CanonicalOverlay) gains three bindings paralleling @sourceRow: tableMethod.className → ClassNameBinding, tableMethod.method → MethodNameBinding(tableMethod.className), tableMethod.argMapping → ArgMappingBinding. Test-fixture migration: every @tableMethod(tableMethodReference: {className: "X", method: "Y"}) occurrence in GraphitronSchemaBuilderTest, ServiceRootFetcherPipelineTest, TableMethodFieldValidationTest, DiagnosticsTest, ClassNameCompletionsTest, and the sakila-example schema flattens to @tableMethod(className: "X", method: "Y"). TestTableMethodStub static methods drop their leading Table<?> parameter. SampleQueryService.popularFilms rewrites from (Film filmTable, Double minRentalRate) to (Double minRentalRate), deriving Tables.FILM internally. ServiceCatalogTest’s `reflectTableMethod_* cases thread the new policy parameter; reflectTableMethod_overrideTargetingTableSlot_rejected now exercises the REQUIRED policy via TestConditionStub.argCondition. TypeFetcherGeneratorTest.queryTableMethodTableField_emittedFetcher_* drops the ParamSource.Table slot from its handcrafted MethodRef. The LSP legacyName_unresolved_tableMethod test retires (name: alias is gone). User-facing doc docs/manual/reference/directives/tableMethod.adoc rewrites the SDL signature and examples to the flat form and adds a "Method-signature contract" section pinning the no-Table-parameter rule. docs/manual/how-to/external-code.adoc updates the per-directive slot-name table to distinguish flat-form (@tableMethod, @sourceRow) from ExternalCodeReference-shaped directives. R43 status was moved Ready → In Progress in 55b5d5f ahead of this commit. Out of scope (kept under remaining R43 commits): path resolution + last-hop-target validation; child table-bound-parent lift (TypeFetcherGenerator.STUBBED_VARIANTS membership for ChildField.TableMethodField remains); new ChildField.RecordTableMethodField variant. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R100 (362719e implementation; 8498b89 In Progress → In Review): LSP coverage for @node(keyColumns:) and @nodeId(typeName:). Per-keystroke responsiveness layer on top of the existing classifier-rejection paint (validatorDiagnostics keeps full coverage at the rebuild tier). Two canonical-overlay deltas plus one new Behavior arm: @node(keyColumns:) → CatalogColumnBinding so FieldCompletions and Diagnostics.validateCatalogColumn auto-fire over the type’s @table-backed jOOQ column list; @nodeId(typeName:) → new Behavior.NodeTypeBinding() siblings-by-keyset to CatalogColumnBinding / CatalogTableBinding, with one new completion provider (NodeTypeCompletions) and one new arm in Diagnostics.dispatch that reads a new CompletionData.nodeMetadata() map. CatalogBuilder walks the assembled schema’s GraphQLObjectType`s and records pre-deduction `(typeId, keyColumns) per @node-bearing type; classifier-deduced values (containing-type / unique-table / PK inference) stay invisible to in-editor feedback by design. LspVocabulary.leafCoordinates / descendLeaves fan out raw list_value AST nodes into one Leaf per scalar element so CatalogColumnBinding dispatches per-element on keyColumns: [...]; the contract pin “Leaf.valueNode` is the scalar value node, never an enclosing list_value” lifts to a universal property of the leaf walk. `Hovers.valueNodeFor mirrors the descent so cursor inside a list element highlights the element, not the whole list; new nodeTypeHover arm renders the target type’s typeId + key-column list with each column’s graphqlType pulled from CompletionData.Column. Two backwards-compat CompletionData constructors retained (existing 3-arg pattern extended with a 4-arg shim for tests not carrying the new map). Tests five wire-shape integration cases (FieldCompletionsTest.nodeKeyColumnsCompletionInsideListLiteralReturnsTableColumns; DiagnosticsTest.nodeKeyColumns_unknownElement_producesError / _allValid_producesNoError, .nodeIdTypeName_unknownType_producesError / _knownNodeType_producesNoError / _emptyNodeMetadata_suppressesUnknownTypeDiagnostic; HoversTest.nodeKeyColumnsHover_insideListElement_showsColumnMetadata / .nodeIdTypeNameHover_resolvesTypeIdAndKeyColumns); per-provider unit cases in NodeTypeCompletionsTest (3); leaf-walk fan-out pin in LspVocabularyTest.leafCoordinates_listValueFansOutOneLeafPerElement; catalog-side NodeMetadata coverage in CatalogBuilderTest (3 cases ; author-supplied capture, omitted-axes-stay-null, non-@node types omitted). Reference docs gain "Editor support" subsections on node.adoc and nodeId.adoc. Out of scope (called out, not regressed): @node(typeId:) cross-schema duplicate validation stays on rebuild path; @nodeId(typeName:) deduction rules (containing-type / unique-table inference) stay invisible to LSP; @node placement and PK-defaulting structural validation stays on the rebuild path; @nodeId(typeName:) diagnostic conflates "type doesn’t exist" and "type exists without @node`" into one message and tests only the unknown-type case (the spec listed both scenarios; the type-exists-without-@node` case is incremental coverage of the same code path and a candidate follow-up); nodeTypeHover.columnGraphqlType does case-insensitive across all tables rather than scoping to the @node type’s @table (latent, not triggered by Sakila; candidate follow-up). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R141 (d58d46c In Progress → In Review carrier-shape lift + new sealed leaf + tests; 83cd67a rework: order-preservation PK-keyed-map indirection in FetcherEmitter.buildSingleRecordTableFetcherValue’s `Cardinality.MANY arm): Admit bulk-input mutations with a single payload carrier wrapping a list-shaped data field. New MutationField.MutationBulkDmlRecordField sealed leaf (sibling of MutationDmlRecordField) classifies (tia.list() == true, dataField.wrapper().isList() == true, kind ∈ {INSERT, UPDATE}); carrier-shape lift introduces sealed CarrierFieldRole (permits DataChannel, ErrorChannelRole) with SingleRecordCarrierShape carrying List<CarrierFieldRole> under compact-ctor invariants (exactly-one DataChannel, at-most-one ErrorChannelRole, distinct field names), consolidating the previously parallel data + error walks into a single unified walk in BuildContext.tryResolveSingleRecordCarrier (the carrier-side resolveErrorChannel call site retires; the standalone method stays for the four non-carrier callers). Compact-ctor on the new leaf rejects DELETE (incorrect-by-construction) and UPSERT (deferred to R145 under R144’s cardinality-safety regime); UPSERT bulk-carrier case surfaces as a classify-time author-facing rejection rather than letting the compact-ctor throw. Single-input + list-data-field rejects as new Invariant #16 inside MutationInputResolver.validateReturnType. Emit strategy: per-row DML inside dsl.transactionResult(...) accumulating PKs into Result<RecordN<PK>> in input order (N+1 statements: N per-row DML + 1 response SELECT); order preservation lifted from a Postgres-scan-order coincidence to a property of the emitted Java via PK-keyed-map indirection in FetcherEmitter.buildSingleRecordTableFetcherValue’s `Cardinality.MANY arm (re-key SELECT result into Map<PK, Record>, iterate the upstream input-ordered Result<RecordN<PK>> to project into List<Record> in input order). UPDATE no-match throws IllegalStateException to keep acc.size() == in.size() invariant. Audit. New load-bearing classifier-check key single-record-carrier-shape.roles-exhaustively-classified (producer on tryResolveSingleRecordCarrier, consumers on GraphitronSchemaBuilder.registerCarrierDataField and TypeFetcherGenerator.buildMutationBulkDmlRecordFetcher); mutation-dml-record-field.data-table-equals-input-table extends across both record-carrier leaves. New CarrierFieldRoleCoverageTest audits permit dispatch across consumers via grep-on-source-name; reflection-based hardening tracked at R151. Tests. Three classifier truth-table rows: MUTATION_BULK_DML_RECORD_FIELD (admit), DML_INSERT_SINGLE_LIST_DATA_REJECTED (Invariant #16), DML_INSERT_LIST_PAYLOAD_NO_CARRIER_FIELD_ROLE_REJECTED. Three execution tests in DmlBulkMutationsExecutionTest: bulkInsertWithThreeRowsInNonPkOrderPreservesInputOrderInResponse (N=3 load-bearing order assertion), bulkInsertWithSingleRowExercisesBulkLeafPath (N=1 sanity), bulkUpdateWithThreeRowsInNonPkOrderPreservesInputOrderInResponse (UPDATE order assertion). Sakila fixture gains FilmsPayload { films: [Film!] } + createFilmsPayload / updateFilmsPayload mutations. Out of scope (deferred): per-row error correlation (R12 flat-error contract preserved); affected-row-count / clientMutationId sibling permits (each is a new CarrierFieldRole permit + classifier rule); UPSERT bulk-carrier admission (R145); @service bulk-carrier symmetric path; sealed-on-kind / sub-taxonomy refactors of both record-carrier leaves. Defers consumer-side @DependsOnClassifierCheck annotations against R12’s planned error-channel.* keys; R12 lands both halves in one commit when it ships, referencing R141’s ErrorChannelRole permit Javadoc as the trust-statement anchor. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R150 (aa7e1b0 implementation; c8bdbe5 SDL-driven invariant tightening; e96a05c SDL Scalar vs Input Object only; bfdde5e permanent Map rejection; 2c2b37a cycle detection + public-class check; bb79b63 polish ; typed LinkageError/ClassNotFoundException, nested-class .→$ retry, createBean/createBeanList plural naming, FieldKey inlined): Instantiate service-layer input beans at the @service fetcher boundary. Closes the silent ClassCastException: LinkedHashMap cannot be cast to <ConsumerBean> gap when a @service method’s Java parameter (single or List<Bean>) is a consumer-authored class mirroring an SDL input type. New CallSiteExtraction.InputBean sealed-variant arm carries the bean ClassName, the Target (RECORD/JAVA_BEAN constructor shape), and per-SDL-field FieldBinding`s. `InputBeanResolver post-processes a resolved MethodRef.Service (sibling to EnumMappingResolver.enrichArgExtractions), driving classification off the SDL side: GraphQL scalar args (including custom scalars wired via @scalarType) stay on Direct so graphql-java’s coercion delivers the consumer’s declared Java type, and GraphQL input-object args classify as InputBean or reject loudly at generation time. Rejections are exhaustive and structural ; non-public bean classes, missing record components, classes without a public no-arg constructor, recursive shapes (guarded by a path-scoped visited set, prevents StackOverflowError at gen time), Map/JDK/org.jooq.*/enum/array element types paired with input-object SDL slots, and list-cardinality mismatches. Map<K, V> is permanently rejected as a service-boundary anti-pattern; consumers wanting open-ended-JSON semantics declare a custom scalar via @scalarType. InputBeanInstantiationEmitter emits one createBean(Map<String, Object>) + one createBeanList(Object) helper per unique bean class on the enclosing *Fetchers class ; dedup-by-class via collectTransitively walks nested input-object leaves. Records use positional canonical-ctor; JavaBeans use no-arg + set<X> setters. ArgCallEmitter.buildArgExtraction routes the InputBean arm to the helper call. Cycle-prevention invariant preserved: helpers reference only JDK types and the consumer’s service-package class; no helper imports a graphitron-emitted record (R94 compatibility). Tests four-tier: L1 TypeFetcherGeneratorTest pins helper signature + record/JavaBean target + createFooList plural naming + transitive dedup; L2 GraphitronSchemaBuilderTest covers singular/list InputBean classification plus SERVICE_MAP_PARAM_FOR_INPUT_OBJECT_REJECTED, SERVICE_RECURSIVE_BEAN_REJECTED, SERVICE_NON_PUBLIC_BEAN_REJECTED arms; L3 graphitron-sakila-example adds FilmReviewDetailsInput + FilmReviewTagInput SDL types and submitFilmReviewWithDetails(details: FilmReviewDetailsInput!) mutation, with consumer-authored FilmReviewDetails record + FilmReviewTag record in graphitron-sakila-service compiling against the generated helper; L4 GraphQLQueryTest.submitFilmReviewWithDetails_routesThroughInstantiatedInputBean round-trips a nested-list-bearing bean through a real GraphQL mutation, asserting service body sees typed scalar values. Out of scope (called out, not regressed): @service parameter as a jOOQ TableRecord subclass currently routes through the JavaBean setter path (not idiomatic record.from(map); tracked as a follow-up); recursion is head-only ; a @service parameter whose argMapping is a multi-segment dot-path stays on the legacy Direct arm even when the leaf SDL type is an input object; SDL oneOf polymorphic inputs and builder-pattern target classes deliberately deferred. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R108 (a60c58a implementation): Per-variant projection on polymorphic fields. The multi-table polymorphic dispatcher’s Stage-2 per-typename SELECT now threads the parent DataFetchingFieldSelectionSet through PolymorphicSelectionSet.restrictTo(source, concreteTypeName) (new emitted helper at <outputPackage>.util.PolymorphicSelectionSet, generated by PolymorphicSelectionSetClassGenerator under generators/util/, registered in GraphQLRewriteGenerator next to ConnectionHelper), so each per-typename SELECT projects only columns whose SelectedField.getObjectTypeNames() contains that participant. The helper is a delegating wrapper that materially overrides only getFieldsGroupedByResultKey() and delegates every other DataFetchingFieldSelectionSet method to the source, keeping the nested-projection recursion in $fields (which walks sf.getSelectionSet()) intact and avoiding a widened $fields signature. Same-table interface emit site at TypeFetcherGenerator.buildInterfaceFieldsList intentionally untouched (per-spec carve-out: the LinkedHashSet dedup masks over-selection in every currently-exercised fixture); javadoc cross-reference notes restrictTo is reusable as-is when a fixture exercises the break-the-dedup shape. Tests four-tier: PolymorphicProjectionFilterPinTest (unit, UnifiedEmissionPinsTest precedent ; folder-wide $T.restrictTo(env.getSelectionSet() count == 1, single-file $$fields(env.getSelectionSet() in MultiTablePolymorphicEmitter.java count == 0); PolymorphicSelectionSetClassEmitTest (pipeline, structural pin of the emitted class ; name, modifiers, restrictTo signature, private no-arg constructor, private static final Filtered nested type implementing DataFetchingFieldSelectionSet); PolymorphicNestingFilterTest (pipeline, asserts exactly one PolymorphicSelectionSet reference per Stage-2 helper body, encoding "no further filter needed at depth"); RecordParentMultiTablePolymorphicPipelineTest extended with an asymmetric-fragment fixture (Inventory + Content sharing filmId backed by different columns on different tables) driving full SDL → classify → emit; PolymorphicProjectionQueryTest (execution, SQL-capture via jOOQ ExecuteListener ; asymmetric-Customer asserts Staff Stage-2 SELECT does not contain "staff"."first_name", asymmetric-Staff pins the inverse, symmetric keeps both; SELECTs picked by the per-typename "customerinput" / "staffinput" VALUES alias to ignore Stage-1’s narrow UNION ALL); GraphQLQueryTest.addressOccupants_asymmetricFragment_responsePayloadDropsInactiveBranch (behavioural pin on the response map). Stage-1 narrow SELECT, DataLoader-batched vs inline arms, requiredProjectionColumns, and synthetic __typename / __sort__ / idx projections all untouched (added outside the $fields call or inside $fields outside the selection switch). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R147 (4fadc3d implementation; 2450971 simplify: drop the backward-compat Diagnostics.compute overload): Surface GraphitronSchemaValidator errors and warnings as LSP diagnostics. GraphQLRewriteGenerator.buildOutput() now runs the validator over the same classified bundle that yields the catalog and snapshot, packaging the result as a new ValidationReport(errors, warnings, sourceUris) record alongside a BuildArtifacts(catalog, snapshot) split of BuildOutput; the precomputed sourceUris canonical-URI set lets Diagnostics.compute short-circuit per file with one Set.contains. Workspace.setBuildOutput(BuildArtifacts, ValidationReport) replaces the old setCatalog / setCatalogAndSnapshot overloads, atomically swapping all three volatile refs; DevMojo routes both the schema-save and classpath triggers through the unified setter, so unresolved-@service-class errors surface in the editor on the next mvn compile without waiting for a schema save. Severity mapping is an exhaustive switch over the Rejection sealed hierarchy (AuthorError / InvalidSchema → Error, Deferred → Warning); BuildWarning maps to Warning. Freshness-aware silence policy mirrors R139: validator diagnostics fire under Built.Current only, silent under Unavailable and Built.Previous. Source attribution is "graphitron-validator", distinct from "graphitron-lsp". Two new paired classifier-check keys (source-location.absolute-path-source-name on RewriteSchemaLoader, validation-report.canonical-uri on ValidationReport.canonicalUri) pin the cross-module invariants the LSP filter relies on. Tests: ValidatorDiagnosticsTest covers severity per Rejection permit, per-file filtering, freshness gating, no-usable-location drop, and the empty-report-clears-previous-diagnostics contract at compute-call level; RejectionSeverityCoverageTest pins exhaustiveness reflectively; WorkspaceTest gets setBuildOutput swap tests; ValidationReportTest covers the from factory and the canonical-URI helper; CatalogRefreshTest migrates to the new setter. Self-review surfaced two follow-up items filed mid-implementation: R148 (source-location-skips-description.md, Backlog/bug) ; FieldDefinition.getSourceLocation() returns the start of the description block when one is present, so diagnostics on documented fields highlight the doc block rather than the field; R149 (r147-followup-end-to-end-publish-diagnostics-tests.md, Backlog/test) ; end-to-end LSP publishDiagnostics wire-test and GraphQLRewriteGeneratorTest for buildOutput() report population were deferred. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R143 (e670fb9): Surface a date column on the rolled-up roadmap. Item gains nullable created: LocalDate / lastUpdated: LocalDate parsed via Item.from.parseDate (accepts both SnakeYAML’s auto-parsed java.util.Date shape and bare-string YAML; absent passes, malformed throws naming slug+key+value). runCreate stamps both dates today; new status subcommand (runStatus + the pure, package-visible applyStatusTransition) resolves slug or R<n> via resolveItemFile, validates target+transition against TARGET_STATES / ALLOWED_TRANSITIONS, writes new status: + fresh last-updated:, leaves created: strictly untouched (never invented for pre-R143 items), and regenerates README; Done and Discarded are rejected as targets per workflow.adoc. Markdown renderActive gains an Updated column; appendBacklogLine emits <sub>updated Y-M-D[, created Y-M-D]</sub> between description and blocked by:. AsciiDoc renderAdocStatusBoard becomes [cols="1,4,1,1,1"] with the new column; backlog adoc emits italic _(updated ...)_; the plan-page attribute box gains Created / Updated rows (suppressed when absent). .claude/skills/roadmap/SKILL.md rewritten to invoke the status subcommand instead of hand-editing front-matter; workflow.adoc gains a bullet on the auto-stamp under "Item file conventions". Tests: RoadmapDateColumnTest (21 cases) covers create stamping, status preserving created in both present/absent shapes, rejection of invalid transitions and of Done / Discarded, slug + R<n> resolution, all four renderer cells in both markdown and AsciiDoc, the plan-page attribute box, and the parser’s "absent passes, malformed fails" semantics. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R140 (036772f): Publish leaf-coverage report from CI. rewrite-build.yml build job grows two trunk-gated steps after mvn verify -Plocal-db ; Regenerate leaf-coverage report runs the roadmap-tool exec against the in-workspace target/leaf-coverage.jsonl traces, then Upload leaf-coverage artifact uploads the regenerated graphitron-rewrite/roadmap/inference-axis-coverage.adoc as the inference-axis-coverage artifact with if-no-files-found: error. Two new jobs in the same workflow: docs-build (needs build, trunk-gated, downloads the artifact over the committed placeholder, builds the docs module with -DskipTests, uploads the Pages artifact) and docs-deploy (needs docs-build, trunk-gated, pages: write + id-token: write, pages concurrency, github-pages environment, actions/deploy-pages@v4). .github/workflows/deploy-docs.yml deleted; the consolidation sidesteps the workflow_run-on-default-branch constraint that blocked the original R132 sketch (rewrite workflow files do not live on main). workflow_dispatch does not survive; manual re-deploy is via the Actions UI re-run on the most recent successful trunk run. inference-axis-coverage.adoc prose updated to describe the live publish chain and explain that the in-git file stays as a non-data placeholder so local doc builds and PR-preview renders find a file at the expected path. Cross-spec: when R133 flips the leaf-coverage profile to opt-in, the Regenerate leaf-coverage report step here will need -Pleaf-coverage added; R133’s own spec already owns that coordination. Pre-merge verification limited to YAML parse + additive/trunk-gated reasoning + PR-run isolation; full deploy-path verification is post-merge against https://sikt-no.github.io/graphitron/roadmap/inference-axis-coverage.html.

  • R142 (04a649b user-directive arms wired through DirectiveResolution; 4ac157e self-review fixes pinning bundled-shadows-snapshot precedence on the hover surface; a39ce93 In Progress → In Review): Phase 2 of the LSP schema-snapshot side-channel (R139 was phase 1). Three more LSP consumers now read the snapshot through the sealed DirectiveResolution.{Bundled | User | Unknown} result and light up on user-declared directives: Hovers.compute surfaces directive-name hovers (pre-coordinate branch on directive.nameNode()) and arg-name docstring fallback from InputValueShape.description(); Diagnostics.compute extends its existing outer-snapshot / inner-resolution switch on Built.Current with validateUnknownArgsAgainstSnapshot + validateRequiredArgsAgainstSnapshot package-private helpers next to the bundled equivalents; ArgNameCompletions.generate grows an LspSchemaSnapshot parameter and routes the User arm through a top-level-only userGenerate helper (nested completion stays empty until the snapshot carries input-object shapes). Hovers and completions are freshness-agnostic (stale info beats silence); diagnostics warn only under Built.Current (mirrors R139’s unknown-directive arm). Bundled-shadows-snapshot precedence (R139 settled design note 4) pinned with parallel guards on all three consumers: Hovers gates user-arm fallback on resolution instanceof DirectiveResolution.User, Diagnostics keeps the existing Bundled early-continue, and the new bundledDirectiveArgHover_ignoresSnapshotShadow / bundledDirectiveShadowedBySnapshot_routesThroughBundledPath cases anchor the guard symmetrically with DiagnosticsTest.bundledArgValidationStillFires_evenWhenSnapshotShadows. Tests. New pipeline cases: HoversTest (6 ; directive-name, arg-name, Unavailable/Previous freshness, shadow guard, bundled side-benefit), DiagnosticsTest (6 ; unknown-arg, missing-required, present-required-silent, Unavailable/Previous silence, shadow guard), ArgNameCompletionsTest (5 ; top-level snapshot args, nested-deferred-empty, Unavailable empty, Previous still emits, shadow guard). Existing bundled-path tests stay unchanged; the snapshot parameter threads LspSchemaSnapshot.unavailable() for tests not exercising the user-arm. No unit-tier additions (each consumer is a thin walker over the records CatalogBuilderSnapshotTest already pins; seal exhaustiveness is javac-checked); no schema-fixture additions to graphitron-sakila-example (R139’s @auth(role: String!) fixture remains the regression guard for the input contract); no execution-tier (LSP behaviour is observable in pipeline outputs). Audit unchanged. No new @DependsOnClassifierCheck markers ; Hovers.compute and ArgNameCompletions.generate are freshness-agnostic so no classifier guarantee is load-bearing for them, and Diagnostics.compute’s existing marker still covers the new arg-validation arms (they inherit the same `Built means clean parse dependency through the same compute body). The R139 prep-note’s "two more markers" expectation was wrong in spirit; the audit-widening decision stays tracked under R139’s "Future evolution". Deferred (called out in the spec body, unchanged): project user-declared input-object types into the snapshot (lights up nested unknown-field validation in Diagnostics and nested arg-name completion in ArgNameCompletions together as a producer-side widening); lift the directive-name hover branch into LspVocabulary once a third consumer wants it. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R138 (a5bc310 In Progress → In Review implementation; bb415a2 plan-body self-review addendum): Extend Invariant #15 to the Payload arm. The pre-R138 classifier admitted bulk-input + single-record-payload DML mutations (e.g. createFilmsPayload(in: [FilmCreateInput!]!): FilmPayload) via R75 Phase 1’s NoBacking-promotion carrier path, bypassing the out-of-band deferred rejection in FieldBuilder.buildDmlField; the generated fetcher ended in valuesOfRows(...).returningResult(...).fetchOne(), which throws TooManyRowsException for every input with >1 row. Fix lifts the listInput && !returnType.wrapper().isList() predicate to MutationInputResolver.validateReturnType’s sealed-root level so it fires uniformly across all three admitted return-type arms (ScalarReturnType(ID), `TableBoundReturnType, ResultReturnType), retires the duplicated per-arm check on the ID and T arms, and rewrites the rejection message to name TooManyRowsException as the runtime failure (replacing the pre-R134 "silent drop of all-but-last-row data" framing). FieldBuilder.buildDmlField’s deferred-rejection block is deleted along with its now-unused `listInput parameter and the four kind-switch call sites. TypeFetcherGenerator.buildMutationDmlRecordFetcher collapses to a single emit shape: the if (tia.list()) empty-list short-circuit block, the dataIsList local, the payloadType ternary, and the .fetch() / .fetchOne() terminator ternary all retire; the fetcher emits a single rowType RecordN<...> via unconditional .fetchOne(), and the Javadoc is rewritten to describe the one remaining shape. graphitron-sakila-example/schema.graphqls drops the createFilmsPayload declaration and its R134 explanatory comment block (the shape is now unclassifiable, so R134’s compilation-tier regression has no surviving anchor ; the principled outcome, not a coverage gap). Tests. New GraphitronSchemaBuilderTest.DML_INSERT_LIST_PLAIN_PAYLOAD_REJECTED row covers the plain-SDL carrier variant; the pre-existing DML_INSERT_LIST_PAYLOAD_DEFERRED row renames to DML_INSERT_LIST_PAYLOAD_REJECTED and retargets its assertion to "must return a list" + "Invariant #15" (both @record-carrier and plain-SDL variants now land at the same validator decision). MUTATION_DML_RECORD_FIELD flips from createFilms(in: [FilmCreateInput!]!) to createFilm(in: FilmCreateInput!); four SingleRecordCarrierPipelineTest fixtures (carrier_listDataField_classifiesAsMutationDmlRecordField, carrier_listDataField_dataFieldClassifiesAsSingleRecordTableField, carrier_atRecordWithNullClassName_classifiesAsMutationDmlRecordField, carrier_withDelete_rejectsAtClassifier) switch from payloadDml (bulk) to payloadDmlSingleInput; the carrier-promotion and trigger-rejection cases keep bulk input because the per-arm rejection fires first. No execution-tier test (the failure mode is TooManyRowsException thrown inside transactionResult; an "asserts throws" test carries no signal beyond classifier rejection). Defers the bulk-carrier-with-list-data-field permit (MutationBulkDmlRecordField-style sealed leaf, Result<...>-keyed response-SELECT, list-element data-field classifier) to a future Backlog item under the slug bulk-input-single-carrier-list-data-field if a real schema surfaces a need. Build green: mvn -f graphitron-rewrite/pom.xml install -Plocal-db.

  • R130 (57d6673 Phases 1–4 implementation; 8f42848 Phase 4 compile- and execute-tier coverage): Admit the two same-table @nodeId-decoded input-field carriers (InputField.ColumnField with CallSiteExtraction.NodeIdDecodeKeys and InputField.CompositeColumnField) in @mutation inputs and @lookupKey bindings. The headline forcing function is composite-PK DELETE shaped like slettRegelverksamling(input: { id: ID! @nodeId @lookupKey }) against a composite-PK table; post-R131 the same-table arm classifies as CompositeColumnField and pre-R130 the MutationInputResolver rejected the carrier outright. Phase 1 (model + extraction-propagation fix): new InputColumnBindingGroup sealed root (MapGroup, DecodedRecordGroup) as a sibling to R50’s LookupArg, rooted at an input-field cluster rather than an outer GraphQL argument; InputField gains sealed LookupKeyField / SetField sub-interfaces permitting ColumnField and CompositeColumnField only (reference carriers stay outside the permits set); TableInputArg.fieldBindings retypes to List<InputColumnBindingGroup> and lookupKeyFields / setFields retype to the new sealed permits; EnumMappingResolver.buildLookupBindings honors the carrier’s cf.extraction() when non-Direct instead of unconditionally re-deriving from raw column metadata. The pre-R130 unconditional re-derivation discarded the resolver-supplied NodeIdDecodeKeys; the R131 follow-up SDL-boundary @nodeId guard (lookup-key-input-field-non-nodeid-decoded) papered over the bug at the cost of rejecting the shape entirely. The fix at source retires that key and replaces it with two new @LoadBearingClassifierCheck keys (mutation-input.lookup-binding-honors-carrier-extraction, mutation-input.lookup-binding-decoded-record-arity-matches-carrier-columns) paired with @DependsOnClassifierCheck consumers on the lookup-WHERE / row-IN / INSERT-arm emitters. Phase 2 (classifier admission): MutationInputResolver admits ColumnField(NodeIdDecodeKeys) and CompositeColumnField in lookup-bearing verbs; reference-carrier rejections reframe as R24-shaped Rejection.deferred(summary, "nodeidreferencefield-join-projection-form"); CompositeColumnField × INSERT carves out via Rejection.deferred(summary, "") (no roadmap item exists today; lifts when a forcing-function schema appears); CompositeColumnField outside @lookupKey position on UPDATE / UPSERT also rejects (the SET-side / INSERT-arm dispatch for composite-PK column writes is out of R130 scope). Phase 3 (emitter dispatch): buildLookupWhereSingleRow lifts a per-row Record<N> decode local to postInGuard with ThrowOnMismatch null handling (GraphqlErrorException on wrong-type id); buildBulkLookupRowIn adopts a block-lambda form for decode-bearing groups (expression-lambda preserved for the all-Direct shape so existing pipeline traces stay byte-identical); INSERT / UPSERT column lists expand CompositeColumnField into its N member columns via buildInsertColumnList; buildPerCellValueList dispatches on carrier identity for the values list; buildInsertDecodeLocals lifts decode locals into preGuard for single-row INSERT / UPSERT (per-row inside the stream lambda for bulk). Phase 4 (tests): classifier tests in MutationDmlNodeIdClassificationTest for composite-PK DELETE / UPDATE / UPSERT admission, INSERT carve-out, and single-PK extraction-propagation; the two R131 follow-up GraphitronSchemaBuilderTest.ArgumentParsingCase.LOOKUP_KEY_ON_NODEID_INPUT_FIELD_REJECTED{,_COMPOSITE_PK} cases retype to LOOKUP_KEY_ON_NODEID_INPUT_FIELD_ADMITTED{,_COMPOSITE_PK} asserting MapInput.bindings[0].extraction is NodeIdDecodeKeys (single-PK) and LookupArg.DecodedRecord.bindings has the expected positional arity (composite-PK). Phase 4 (compile + execute) (8f42848): sakila-example surfaces DeleteFilmActorByNodeIdInput @table(name: "film_actor") with id: ID! @nodeId(typeName: "FilmActor") @lookupKey plus Mutation.deleteFilmActorByNodeId and Mutation.deleteFilmActorsByNodeId driving both buildLookupWhereSingleRow’s `DecodedRecordGroup arm and buildBulkLookupRowIn’s block-lambda arm end-to-end; new `keyed_node(id varchar PK, label varchar) table in init.sql plus KeyedNode @node type and Mutation.createKeyedNode driving the ColumnField(NodeIdDecodeKeys) INSERT-arm (buildInsertDecodeLocals preGuard local + buildPerCellValueList NodeIdDecodeKeys arm); five execution-tier tests in DmlBulkMutationsExecutionTest covering composite-PK DELETE single-row, composite-PK DELETE bulk row-IN, single-PK INSERT round-trip, and ThrowOnMismatch on both the lookup-key and INSERT-arm paths. Deferred (acknowledged scope reduction, not rework): composite-PK UPDATE / UPSERT execution-tier proofs (the single-row decode-local lift is shared verb-agnostically across DELETE / UPDATE / UPSERT; classifier-tier admission is pinned in MutationDmlNodeIdClassificationTest.compositePkNodeIdLookupKey_{update,upsert}_admitted); reference-carrier admission stays R24-coupled (no forcing-function schema today). Retired key: lookup-key-input-field-non-nodeid-decoded retires producer-only (zero @DependsOnClassifierCheck consumers; LoadBearingGuaranteeAuditTest surfaces no orphan). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25; 23 tests in DmlBulkMutationsExecutionTest pass, 10 in MutationDmlNodeIdClassificationTest pass.

  • R75 (dbffee9 Phase 1 reshape; 862bc86 Phase 1 direct-@table two-step emit + durability pins; 2fd7598 Phase 1 follow-up SELECT coverage; 88df99a Phase 2 foundation; be26134 Phase 2 lean + R137 carve-out; 2408634 + 031c6f3 Phase 2 review fixes): Plain payload types for DML mutations. Replaces the earlier wire-format-unwrap design (PassthroughDataField permit + IdentityPassthrough capability + BuildContext.resolveReturnType short-circuit, all retired) with the structural model the SDL implies: plain SDL Object carriers promote to a new PojoResultType.NoBacking arm at type-classification time, payload-returning DML mutations classify as MutationField.MutationDmlRecordField (DELETE rejected at classify time via the compact constructor), and the data field on the carrier classifies as the new ChildField.SingleRecordTableField sibling permit with an inline SourceKey (new Reader.ResultRowWalk permit on R38’s sealed Reader interface, Wrap.Record, empty path, PK columns from the input @table; cardinality from the data field’s wrapper). DML emit becomes two-step uniformly across carrier and direct-@table shapes: PK-only RETURNING inside dsl.transactionResult(tx -> DSL.using(tx)....), then a follow-up SELECT outside the transaction lambda ; field errors during traversal cannot undo the DML. Phase 2 (lean) widens the trigger to admit record-backed ResultType elements via a sealed DataElement sub-taxonomy (Table / Record); record-element data on @service mutations classifies as the new ChildField.SingleRecordIdentityField permit (identity-passthrough emit, no SourceKey, no SELECT), and DML mutations reject record-element carriers at classify time. Audit. Two new @LoadBearingClassifierCheck keys (mutation-dml-record-field.data-table-equals-input-table and source-key.result-row-walk-wrap-record-empty-path) pair with consumers on the mutation-fetcher RETURNING emit and the data-field response-SELECT emit. Tests. Pipeline-tier SingleRecordCarrierPipelineTest covers per-DmlKind admission, full SourceKey shape, PojoResultType split, DELETE rejection, trigger rejections, table-equality rejection, the structural two-step-emit pin on direct-@table returns, the fetcher-emitter arm-count pin, Phase 2’s record-element classification, the parameterised record-element DML rejection (INSERT/UPDATE/UPSERT), and the SingleRecordIdentityField arm pin. Execution-tier SingleRecordCarrierDmlTest covers round-trip for INSERT / UPDATE / UPSERT (new and existing rows) against sakila plus selection-set strength tests (auto-PK, DB default, @reference projection, post-UPDATE state read), plus the headline durability pins dml_persists_when_followupSelect_throws (carrier) and dml_persists_when_directReturnSelect_throws (direct-@table) via the synthetic DurabilityErrorService.synthesize mid-traversal throw. GraphitronSchemaBuilderTest.NonTableParentCase gains SINGLE_RECORD_CARRIER_DATA_FIELD, SINGLE_RECORD_IDENTITY_FIELD, and MUTATION_DML_RECORD_FIELD. Compilation-tier sakila fixture Mutation.createFilmCard(filmId: Int!): SingleFilmCardCarrier wires the @service-mutation + NoBacking-carrier + record-element-data-field shape end-to-end. Carved out to R137 (service-wrapper-composition): the 8-case execution matrix over {T, Optional, CompletableFuture, Mono, DataFetcherResult} × {Table, Record} and the data-element-aware strict service-return validator, both blocked on the @service substrate admitting wrapper layers on method return types. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25 across all 11 modules.

  • R131 (a64cd8f + 1acbaa0 + fe2de55 + beb0e92): Collapse the singular id: ID! @nodeId input-field classifier onto NodeIdLeafResolver.resolve so both arities (ID! and [ID!]) consume the same sealed Resolved outcome (SameTable | FkTarget.DirectFk | FkTarget.TranslatedFk | Rejected) through a single shared helper BuildContext.inputFieldFromNodeIdResolved. Pre-R131 the singular branch open-coded typeName inference, schema/catalog lookup, path parse, validateLift, and liftSourceColumns, then funneled every outcome into the Reference-only buildInputNodeIdReference sink ; even on the canonical same-table case where the leaf semantically filters the parent’s own rows by primary key. The reproducer schema (SlettRegelverksamlingInput @table(name: "regelverksamling") { id: ID! @nodeId } on a composite-PK table) now lands on CompositeColumnField (same-table arm), not CompositeColumnReferenceField. The duplicate BuildContext.NodeIdTypeNameInference record and inferNodeIdTypeName helper retire ; NodeIdLeafResolver.inferTypeName is the single home. buildInputNodeIdReference survives only as the id-reference synthesis shim’s sink and is documented as such. Audit in the same commit: producer @LoadBearingClassifierCheck("nodeid-fk.direct-fk-keys-match") and consumer @DependsOnClassifierCheck annotations on BuildContext.classifyInputFieldInternal and FieldBuilder.walkInputFieldConditions rewritten to describe the post-R131 shape; the stale "`CompositeColumnReferenceField` may represent a same-table PK filter" reading is gone (MutationInputResolver rejects all four @nodeId-decoded input-field carriers as deferred; R130’s post-R131 pivot retains scope for the same-table column-direct half ; ColumnField with NodeIdDecodeKeys and CompositeColumnField, the carriers the post-R131 classifier produces from SlettRegelverksamlingInput-shaped schemas ; and defers the genuinely-joined *ReferenceField half per R24’s "wait for forcing-function schema" discipline). Tests: NodeIdPipelineTest four-corner pipeline pins for singular + (same-table | FkTarget.DirectFk) × (single-PK | composite-PK) ; InputCase.EXPLICIT_NODE_ID_DIRECTIVE updated to CompositeColumnField, new EXPLICIT_NODE_ID_DIRECTIVE_SINGLE_PK → ColumnField, new InputReferenceCase.REFERENCE_TO_COMPOSITE_PK_NODE_TYPE → CompositeColumnReferenceField with positionally-aligned liftedSourceColumns, NODE_TARGET_NO_METADATA_PK_FALLBACK updated to ColumnField, new ArgumentSameTableNodeIdCase.SAME_TABLE_SCALAR_COMPOSITE_PK pinning FieldBuilder.classifyArgument emits BodyParam.RowEq over the parent’s PK columns; MutationDmlNodeIdClassificationTest.nodeIdFieldInInput_deferred rejection text shifts to CompositeColumnField. Compilation tier (sakila example): new FilmActorSingularNodeIdFilter @table(name: "film_actor") { id: ID! @nodeId(typeName: "FilmActor") } plus Query.filmActorBySingularCompositeNodeId(filter:) emits DSL.row(table.ACTOR_ID, table.FILM_ID).eq(id). CompositeDecodeHelperRegistry.buildHelper drive-by switches the singular non-list branch from var r = ... to a typed Record<N> declaration via a new typedRecord helper (caught by GeneratedSourcesLintTest.varGuard once the singular composite-PK path reached this branch for the first time). Reachability claims (multi-hop, condition-step, and TranslatedFk rejections) hold no-op-by-construction via the shared route; pipeline tests anchor on the resolver’s shared marker constants (LIFT_FAILURE_MARKER, CONDITION_STEP_MARKER), not on copied substrings. Follow-up 1acbaa0 (FK-target / NodeType-keyColumns permutation): NodeIdLeafResolver.permutationToKeyColumns replaces the strict sameColumnsBySqlName predicate ; when the terminal hop’s target-side columns equal the NodeType’s @node(keyColumns:) as a multiset (any order), the DirectFk arm permutes joinPath.liftedSourceColumns() into keyColumns order before constructing the carrier; the @LoadBearingClassifierCheck description rewrites "positionally match" to "equal as a multiset, in any order" and tightens the carrier guarantee. Pinned by InputFieldFkTargetNodeIdCase.FK_TARGET_REORDERED_KEY_PERMUTATION_DIRECT_FK{,_SINGULAR} over the new reordered_pk_parent/reordered_fk_child fixture (declared FK target order (pk_b, pk_c, pk_a), NodeType keyColumns [pk_a, pk_b, pk_c]). Resolves a latent zero-rows regression in the downstream opptak-subgraph regelverksamlingId schema. Follow-up fe2de55 + beb0e92 (@lookupKey composition guard): post-R131 the singular same-table @nodeId carrier is InputField.ColumnField (or CompositeColumnField), so EnumMappingResolver.buildLookupBindings’s pre-R131 structural rejection no longer fires; `beb0e92 moves the guard upstream to sdlField.hasAppliedDirective(DIR_NODE_ID) so both arities surface the same diagnostic ("expose the decoded key column(s) explicitly via @field instead, or move @lookupKey to the outer argument"). New LoadBearingClassifierCheck key lookup-key-input-field-non-nodeid-decoded, new test cases GraphitronSchemaBuilderTest.ArgumentParsingCase.LOOKUP_KEY_ON_NODEID_INPUT_FIELD_REJECTED{,_COMPOSITE_PK}. NodeIdLeafResolver.validateLift / liftSourceColumns tightened from package-private to private (no external callers after the routing collapse). Two follow-on Backlog stubs filed for material gaps surfaced in self-review: R135 (multi-hop @nodeId permutation pipeline test, to prove the multi-hop case the commit asserts works by construction) and R136 (execution-tier round-trip for the FK-permutation case via graphitron-sakila-example + GraphQLQueryTest). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25, 1900+ tests passing.

  • R42 (506c704 + 5d90719): Lift ChildField.ColumnReferenceField out of TypeFetcherGenerator.STUBBED_VARIANTS for the CallSiteCompaction.Direct + FK-only path. New InlineColumnReferenceFieldEmitter builds the $fields switch-arm body as a single-column correlated subquery (DSL.field(DSL.select(<terminalAlias>.<COL>).from(<terminalAlias>).join(...).where(<correlation>).limit(1)).as("<fieldName>")), mirroring InlineTableFieldEmitter’s shape collapsed to a scalar (DSL.field, not `DSL.multiset). TypeClassGenerator.$fields gains a case ChildField.ColumnReferenceField crf arm; TypeFetcherGenerator moves the leaf from STUBBED_VARIANTS into PROJECTED_LEAVES and the fetcher switch arm becomes a no-op. FetcherEmitter wires new ColumnFetcher<>(DSL.field("<name>")) for the Direct shape. The two non-lifted shapes surface at build time, not runtime: GraphitronSchemaValidator.validateColumnReferenceField rejects NodeIdEncodeKeys with Rejection.Deferred keyed to nodeidreferencefield-join-projection-form (R24) and any JoinStep.ConditionJoin-in-path with Rejection.Deferred keyed to the newly-allocated R129 (column-reference-on-scalar-field-condition-join). The validator/emitter contract carries the @LoadBearingClassifierCheck / @DependsOnClassifierCheck annotation pair (keys column-reference-field-no-nodeid-encode-keys and column-reference-field-no-condition-join-step), so LoadBearingGuaranteeAuditTest walks the dependency and the FetcherEmitter NodeIdEncodeKeys runtime stub for ColumnReferenceField retires as defence-in-depth without an annotated guarantee (CompositeColumnReferenceField’s parallel arm is unaffected; R24 still owns it). Tests: `ColumnReferenceFieldValidationTest restructured for the four-shape matrix (Direct + FK-only passes; Direct + ConditionJoin and NodeIdEncodeKeys + FK-only get the deferred messages; empty path keeps the structural "path is required" error); NestingFieldValidationTest stubbed-nested cases switched to CompositeColumnReferenceField (which remains stubbed); new pipeline-tier ColumnReferenceFieldPipelineTest covers single-hop and multi-hop projection plus ColumnFetcher wiring; sakila-example schema adds Film.languageName: String @field(name: "NAME") @reference(path: [{key: "film_language_id_fkey"}]); GraphQLQueryTest.films_languageName_resolvesViaScalarReference covers the execution tier against PostgreSQL. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R43 (scoping change, this commit): The scalar/enum-return form of @tableMethod (originally tracked as the carve-out under tablemethod-scalar-return.md) is closed by rejecting the shape at classification rather than implementing it. TableMethodDirectiveResolver now rejects any non-TableBoundReturnType return as a structural schema error ("@tableMethod requires a @table-annotated return type") at both root and child sites; the previous gating on isRoot is gone, and the resolver’s sealed Resolved collapses to {TableBound, Rejected} (the NonTableBound arm was the only producer of TableMethodField with a non-table return and is dead code now). ChildField.TableMethodField.returnType() is tightened from ReturnTypeRef to ReturnTypeRef.TableBoundReturnType to express the classifier guarantee in the model. TypeFetcherGenerator.STUBBED_VARIANTS keeps the TableMethodField entry with reworded summary ("child @tableMethod (table-bound return) not yet implemented") and a renamed planSlug tablemethod-child-table-bound; the roadmap file is renamed to match and the item body now scopes R43 narrowly to the table-bound child case (QueryField.QueryTableMethodTableField at the root already ships in IMPLEMENTED_LEAVES). Pipeline tests in GraphitronSchemaBuilderTest.UnclassifiedFieldCase gain two cases (TABLEMETHOD_AT_ROOT_WITH_SCALAR_RETURN_REJECTED, TABLEMETHOD_ON_CHILD_WITH_SCALAR_RETURN_REJECTED) asserting the exact rejection message on both sites; TableMethodFieldValidationTest fixtures stop constructing the variant with ScalarReturnType (no longer reachable from real classification) and use TestFixtures.tableBoundFilm instead. Rationale: @tableMethod’s purpose is to bind a developer-authored jOOQ table method, which by construction returns a generated jOOQ table class. A scalar/enum return cannot be made to work; calling the shape "deferred" misled authors with a roadmap link to functionality that would never arrive. Build green: full `mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R101 (ef66e8b Phase 1; fbe354e Phase 2; 46f08f8 Phase 3; this commit Phase 4): Custom-scalar Java type configuration. The hardcoded five-site spec-built-in switch (ServiceCatalog.mapToJavaTypeName, FieldBuilder.mapGraphQLTypeToReflectType, RowsMethodShape.standardScalarJavaType, AppliedDirectiveEmitter.emitInputType, GraphitronSchemaClassGenerator’s literal `.additionalType(...) block) is retired in favour of a single ScalarTypeResolver source-of-truth carrying a sealed ScalarResolution.{Resolved | Rejected} outcome. Consumers bind a custom scalar by pointing at a public static final GraphQLScalarType constant on the classpath, either by directive (scalar Money @scalarType(scalar: "com.example.Scalars.MONEY")) or by the graphql-java-extended-scalars convention table (scalar BigDecimal resolves to ExtendedScalars.GraphQLBigDecimal whenever the artifact is on the consumer’s compile classpath, no directive needed). Graphitron reflects on the constant’s Coercing<I, O> parameters to recover the Java type, and emits .additionalType(...) automatically. Migration: consumers running on the rewrite must remove their manual .additionalType(ExtendedScalars.GraphQLBigDecimal) / .additionalType(consumerScalar) calls from buildSchema(...) hooks for any scalar graphitron now resolves; graphql-java’s GraphQLSchema.Builder.additionalType rejects duplicate type names at build time, so leaving the call in turns into a SchemaProblem rather than silent tolerance. Resolution order: spec built-ins (Int, Float, String, Boolean, ID) win; @scalarType beats the convention layer; convention lands when the SDL name matches an entry on ScalarTypeResolver’s 30-entry table and `graphql.scalars.ExtendedScalars is on the classpath. Unresolved → hard validation error pointing at @scalarType(scalar:) or extended-scalars as the fix; no silent fallback to Object. Phase 4 housekeeping: LSP completion on @scalarType(scalar: |) suggests convention-table FQNs (preferring the entry that matches the enclosing scalar’s SDL name); LSP diagnostics surface malformed-FQN and unknown-class cases inline against the catalog’s external-reference scan; new Behavior.ScalarTypeBinding arm on the @scalarType(scalar:) coordinate; Documentation/code-generation-triggers.adoc gains a @scalarType row; the scalar resolution story is documented in the manual reference page. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R127 (8310179 + 6308198): Accept List<XRecord> as well as Result<XRecord> at root @service on a @table-bound list return. The classifier’s strict catalog-side TypeName.equals check now returns null from ServiceDirectiveResolver.computeExpectedServiceReturnType for the TableBoundReturnType + List arm, and a new resolver-side validateRootListTableBoundReturnPair rejects any reflected method-return that isn’t exactly org.jooq.Result<XRecord> or java.util.List<XRecord> (graphql-java treats both identically; Result extends List). TypeFetcherGenerator.buildQueryServiceTableFetcher and .buildMutationServiceTableFetcher read MethodRef.returnType() for the List arm so the generated local declaration tracks whichever shape the developer chose. Single cardinality stays strict via the catalog. Annotations. A new LoadBearingClassifierCheck key service-resolver-root-list-record-return-pair owns the resolver-side pair check; the existing service-catalog-strict-service-return description narrows to the Single arm + ResultReturnType paths; both root emitters declare both keys via @DependsOnClassifierCheck. Tests. TestServiceStub.getFilmsAsList returns List<FilmRecord>; ServiceRootFetcherPipelineTest gains a positive case (serviceWithListOfRecordReturn_isAccepted) and a negative case (serviceWithWrongInnerGenericOnList_surfacesAsValidationErrorWithPairedShapes) asserting the rejection names both accepted shapes, the actual mismatched shape, and carries the "service method could not be resolved — " prefix the Single-arm rejection wears. LoadBearingGuaranteeAuditTest covers the new key automatically. Workflow note. The item was filed directly In Progress: the inbound was framed as an operational bug report and the agent began implementation before the Backlog → Spec → Ready gate; CLAUDE.md was tightened in the same branch so the next operational-looking inbound (stack trace plus "make it accept X") doesn’t slip past. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25, 1564 tests passing.

  • R68 (f5c75ef; Phases 1a–6 SHAs compressed by upstream rebase): Diataxis user manual absorbs the legacy graphitron-codegen-parent/graphitron-java-codegen/README.md into the deployed site. Five top-level chapters under /docs/manual/ (index, tutorial, how-to, reference, explanation) mirror the Diataxis quadrants. Tutorial (six pages anchored to graphitron-sakila-example) verified by TutorialSmokeTest ; a @QuarkusTest inside the example module replaying each page’s HTTP query against the JAX-RS endpoint. Reference: 26 directive pages 1:1 against directives.graphqls (drift-pinned by DirectiveDocCoverageTest); mojo-configuration.adoc reflected from the Mojo’s @Parameter-annotated fields (MojoDocCoverageTest); diagnostics-glossary.adoc covering the 16-code RejectionKind/AttemptKind/EmitBlockReason closed set (DiagnosticsDocCoverageTest); deprecations.adoc extracted from SDL @deprecated() markers with @index allow-listed for the GraphQL-spec-disallowed whole-directive case (DeprecationsDocCoverageTest); plus runtime-api.adoc and special-interfaces.adoc as hand-curated prose. How-to: 14 recipe-shaped pages with "verified by" pointers into the graphitron-sakila-example/src/test/java/.../querydb/ consumer test surface (including test-your-schema.adoc, net-new prose with no legacy precedent). Explanation: six pages (why-database-first, why-jooq-and-graphql-java, how-it-works, classifier-mental-model, batching-model, design-decisions). Cutover (Phase 6): docs/quick-start.adoc:15 flipped from the legacy GitHub README pointer to the in-tree xref:manual/reference/directives/index.adoc. Rework pass (f5c75ef): cleared four R<n> leaks from user-facing prose flagged by the In Review reviewer ; R47 reference in external-code.adoc:116 dropped, R114 in multi-hop-nodeid-filter.adoc:15 rephrased to feature-by-name, the until R61 historical-Invariant bullet in result-types.adoc:142 dropped entirely, and the R75: Javadoc prefix on PassthroughDataField in ChildField.java:361 stripped at source (the migration fragment and roadmap/inference-axis-coverage.adoc regenerated). The Phase 1a–6 implementation SHAs the spec body cited (3afc278, fa36dbc, d0c63c4, 8f7d412, 1ea0855, 868593a, d796c4c, 3f6ec55, 23c2056, 863d8be) no longer resolve in local history ; compressed by an upstream rebase before the rework cycle. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25 with 312 + 25 tests passing, all five drift-protection verifiers green.

  • R44 (b978e69): Deprecate @multitableReference, mirroring the @notGenerated removal end-to-end. The directive stays SDL-declared in directives.graphqls so consumer schemas still parse, but FieldBuilder.classifyField now rejects every application with an UnclassifiedField carrying a Rejection.directiveConflict(List.of(DIR_MULTITABLE_REFERENCE), "@multitableReference is no longer supported. Remove the directive; the rewrite generates multi-table interface dispatch from @discriminate / @discriminator without an explicit multitable-reference path."). The rejection is ordered above detectChildFieldConflict so the deprecation message wins over a mutual-exclusivity reason when the field also carries a conflicting directive (the load-bearing ordering invariant the spec called out, mirroring @notGenerated’s precedent). Model. `ChildField.MultitableReferenceField record and its permits entry deleted; sealed-switch exhaustiveness propagates the removal to GraphitronSchemaValidator (dispatch arm + validateMultitableReferenceField method gone) and TypeFetcherGenerator (STUBBED_VARIANTS entry + dispatch arm gone, with the [deferred] multitable-reference-on-scalar slug retiring as a dead anchor). detectChildFieldConflict’s mutual-exclusivity slot list drops `DIR_MULTITABLE_REFERENCE as dead vocabulary; the BuildContext.DIR_MULTITABLE_REFERENCE constant, its PASSTHROUGH_FORBIDDEN_DATA_FIELD_DIRECTIVES membership, and the SchemaDirectiveRegistry.GENERATOR_ONLY_DIRECTIVES entry all retained per the deprecated-but-membership-retained precedent (the SDL declaration is still present). Tests. Pipeline-tier MultitableReferenceFieldCase rewritten from "produces a MultitableReferenceField`" to two cases: `REJECTED asserts UnclassifiedField with the deprecation reason, and REJECTED_WINS_OVER_CONFLICT pairs @multitableReference with @service and asserts the deprecation reason wins over the mutual-exclusivity reason (locking the ordering invariant). The redundant MULTITABLE_REFERENCE_AND_SERVICE_CONFLICT case retires from the child-field conflict suite; MultitableReferenceFieldValidationTest deleted outright. Docs. directives.graphqls description rewritten in the @notGenerated removal shape; docs/manual/reference/directives/multitableReference.adoc rewritten as a deprecation page (opener, SDL signature, Migration, Diagnostic, Constraints, See also); new === @multitableReference subsection under "Hard removals" in migrating-from-legacy.adoc; cross-references to @multitableReference retargeted to @discriminate / @discriminator in reference.adoc, join-with-references.adoc, polymorphic-types.adoc, notGenerated.adoc, and the directive indices (entry moved to the "Rejected by the rewrite" category, gains the _(rejected, remove from the schema)_ annotation); dispatch table row in code-generation-triggers.adoc rewritten in the @notGenerated row’s shape. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R124 (7882caf + f8d5300): Source the codegen reflection classpath from the project’s compile classpath + reactor sibling target/classes, not the plugin realm. AbstractRewriteMojo.withCodegenScope builds a URLClassLoader over project.getCompileClasspathElements() plus the existing resolveClasspathRoots() set, parented on the plugin loader; the loader is threaded through a new RewriteContext.codegenLoader field to the 22 in-process Class.forName(name, false, loader) sites and also installed as TCCL for the duration of the scope (defense-in-depth for third-party transitive callees), restored in finally, and closed to release JAR file descriptors (matters for DevMojo’s per-cycle regeneration). The lone `DataFetchingEnvironment reflection in ClassAccessorResolver stays plugin-internal. API surface: RewriteContext gains a non-null codegenLoader field (eight-arg compact + seven-arg + six-arg back-compat overloads default it to TCCL for unit-tier callers); BuildContext.codegenLoader() is a thin passthrough mirroring nodeIdLeafResolver() and BuildContext.ctx is now @NonNull-enforced via Objects.requireNonNull (the three unit-tier tests that previously passed (null, _, null) now construct a deterministic stub via the 6-arg overload); JooqCatalog takes a (String, ClassLoader) constructor with a one-arg TCCL-defaulting back-compat overload; CheckedExceptionMatcher.unmatched/covers and ServiceCatalog.argExtraction gain a ClassLoader parameter; TypeBuilder.validateExceptionClass and FieldBuilder.checkDeclaredCheckedExceptions flip from static to instance (single same-class callers; the explicit-parameter sibling lives where it crosses a class boundary). Migration: <plugin><dependencies> blocks deleted from graphitron-sakila-example/pom.xml and the basic-generate IT pom; the IT now declares graphitron-sakila-db as a normal top-level <dependency>, locking the contract in the IT itself. Tests: new pipeline-tier CodegenLoaderTest stages a hand-rolled .class file (Java 17 encoding inlined as byte[], so the test does not need a compiler on its own classpath) under a fake target/classes directory, wires its path through project.getCompileClasspathElements(), and asserts (a) the staged class is not on the test JVM’s classpath, (b) inside withCodegenScope the ctx.codegenLoader() resolves it, (c) the TCCL inside the scope is the codegen loader, (d) the previous TCCL is restored after. The compile- and execution-tier load-bearing migration test is the graphitron-sakila-example reactor build, which now compiles and runs against the live schema with no <plugin><dependencies> block. Docs: new "Codegen classpath" section in docs/manual/reference/mojo-configuration.adoc names the new contract and the rare legitimate <plugin><dependencies> case (pinning a different version through the parent chain). Architect-review tightening (f8d5300) added a load-bearing comment to DevMojo.execute explaining why the captured initialCtx must only be read for path-shaped fields (its loader is closed by the time setup proceeds), and one-line policy notes on the two instance-method helpers explaining why they’re not static and why the cross-class siblings take an explicit ClassLoader parameter instead. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • R83 (b3c5b6c + 1187fa5): Pipeline + compilation + execution tier coverage for the multi-schema jOOQ fixture R78 introduced. Three independent tiers ride the multischemafixture catalog (multischema_a + multischema_b, the cross-schema FK gadget → widget, the colliding event table). Pipeline tier: MultiSchemaPipelineTest (annotated @PipelineTier, in graphitron/src/test) drives a slim SDL through GraphitronSchemaBuilder against jooqPackage=multischemafixture and asserts at two typed surfaces. Model-level: TableRef.tableClass() segmentation for the unique-per-schema (Widget → multischema_a.tables.Widget), qualified (@table(name: "multischema_a.event") → multischema_a.tables.Event), and cross-schema-FK-target (Gadget → multischema_b.tables.Gadget) resolution paths; cross-schema JoinStep.FkJoin.fk().keysClass() routes to multischema_b.Keys (the FK-holder schema, not the target’s schema ; the R78 bug case); firstHop.targetTable().tableClass() lands on multischema_a.tables.Widget. Structural emit-side: a typed walk over TypeSpec.methodSpecs[].parameters[].type / returnType / fieldSpecs[].type plus parsed imports from the rendered JavaFile, asserting every ClassName whose canonical name starts with the multischema-fixture root lives under a multischema_a / multischema_b sub-package ; the bare-root R78 bug shape (<jooqPackage>.tables.X, <jooqPackage>.Keys, <jooqPackage>.Tables) cannot appear anywhere typed-reachable from a TypeSpec. Targeted positive assertions read parameter types directly off QueryConditionsGenerator and TypeClassGenerator output. Compilation tier: third rewrite-generate-multischema graphitron-maven-plugin execution in graphitron-sakila-example/pom.xml consuming a new src/main/resources/graphql/multischema.graphqls (the same three shape cases as the pipeline test) and writing to a disjoint no.sikt.graphitron.generated.multischema output package. The full mvn install -Plocal-db reactor compiles the multischema slice against the live multi-schema jOOQ catalog; a regression that re-derives a ClassName from the bare jooqPackage emits source that does not exist (root.Keys / root.tables.Widget never resolve under multi-schema codegen) and the maven-compiler-plugin fails the build. Execution tier: MultiSchemaQueryTest (annotated @ExecutionTier, in graphitron-sakila-example/src/test) loads the multischema slice’s own Graphitron.buildSchema(...) and issues three queries against the rewrite_test PostgreSQL ; the cross-schema FK round-trip (gadgets { gadgetId note widget { widgetId name } }), the schema-A unique resolution (widgets), and the qualified-form resolution (events). Seed rows added to init.sql for one widget, two gadgets pointing at it, plus one row in each event collision table. Self-review pass (1187fa5): the principles-architect agent flagged that the original b3c5b6c rendered-text substring scans (assertThat(rendered).contains("...multischema_a.tables.Widget") plus a leading-space negative form to disambiguate root-truncated FQNs from correctly-segmented ones) were structurally indistinguishable from the body-content assertions banned at every tier. Replaced with the typed walk above; positive assertions now read parameter types directly (no string scan, no JavaPoet import-vs-inline coin-flip). The R78 invariants (TableRef.tableClass(), ForeignKeyRef.keysClass()) fit the @LoadBearingClassifierCheck / @DependsOnClassifierCheck pattern but the global annotation-pair sweep was deferred to R125 to keep R83’s scope on "make the multi-schema fixture earn its keep" rather than expanding into a broader principle-enforcement sweep. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25, all 11 reactor modules SUCCESS, both new test classes pass (7 pipeline tests + 3 execution tests, plus the R78 unit-tier JooqCatalogMultiSchemaTest).

  • R86 (a53502f + ede87f1): Architecture chapter for the user-manual site. New typed-rejection.adoc consolidates the sealed Resolved narrative across the thirteen *DirectiveResolver siblings, the Rejection taxonomy (AuthorError.{UnknownName | Structural | AccessorMismatch}, InvalidSchema.{DirectiveConflict | Structural}, Deferred), and the BuildContext.candidateHint Levenshtein contract; D10 draws the Rejection sealed hierarchy with LookupKeyDirectiveResolver.Resolved.{Ok, Rejected} overlaid as a worked example. README.adoc rewritten as an intent-routed chapter index with D1 (build pipeline). getting-started.adoc § Federation and § Dev loop each gain a marked === How this is wired (for contributors) subsection (D9 federation entity flow, D7 dev-loop runtime framing). runtime-extension-points.adoc § Where the interface comes from prepended with the per-app-emission rationale plus D4 (request lifecycle); code-generation-triggers.adoc § Scope gains D3 (scope state machine). Four manual xrefs restored (explanation/index, classifier-mental-model, how-it-works, how-to/test-your-schema). New SealedHierarchyDocCoverageTest walks Rejection.permits() transitively with bidirectional drift protection ; alternation built from the live permit set so future top-level branches extend coverage automatically. ManualXrefIntegrityTest extended to remap rendered architecture/ paths back to graphitron-rewrite/docs/ (mirrors the stage-architecture block’s README.adoc → index.adoc rename) so source-tree resolution stays honest without staging. rewrite-design-principles.adoc framing line and the Builder-step-results-are-sealed section collapsed to forward pointers into typed-rejection.adoc. Two acknowledged deviations: wire-format-encoding principle stayed in rewrite-design-principles.adoc (page name and consolidation list are about typed rejection, not wire-format decode at the DataFetcher boundary), and existing tables in code-generation-triggers.adoc weren’t converted to enriched form (read fine as-is). Build green; all five *DocCoverageTest siblings pass.

  • R119 (531495a + 6a644f1 + 698a21a + 22dfb8a + 54fc7c9 + 4ae827d + 04daf62 + cc5417c): LSP completion / diagnostics keyed by GraphQL schema coordinates. Sealed SchemaCoordinate (Directive / DirectiveArg / InputType / InputField) plus sealed Behavior overlay (ClassNameBinding / MethodNameBinding(classNameCoord) / CatalogTableBinding / CatalogColumnBinding / CatalogFkBinding / ArgMappingBinding) live in a new LspVocabulary record that wraps a parsed TypeDefinitionRegistry of the bundled directives.graphqls. LspVocabulary constructor enforces the structural invariant ; every overlay coordinate must resolve against the registry ; and throws LspStartupException otherwise; DriftDetectionTest builds the production overlay against the real SDL so R110-style drift is a startup failure, not a silent unknown-directive at request time. Single RewriteSchemaLoader.directivesSdl() accessor consolidates what was two private constants. Seven consumers (Diagnostics, Hovers, ClassNameCompletions, MethodCompletions, FieldCompletions, TableCompletions, ReferenceCompletions) migrate to behavior-arm dispatch via LspVocabulary.coordinateAt(directive, pos, source) + behaviorAt(coord); the @sourceRow gap R110 left in place closes here as a side-effect of the unification. Five DX wins fall out of the parse: unknown-directive / unknown-arg / required-arg diagnostics (Warning severity) in Diagnostics, arg-name completion in a new ArgNameCompletions provider chained last in coordinateBasedCompletions, and SDL-docstring fallback hover via LspVocabulary.descriptionOf. Deletions: DirectiveDefinitions.java (124 lines), DeprecationMarkers.java (164 lines, two regex patterns), SdlAction.DeprecationTarget collapses into SchemaCoordinate. SdlActionDriftTest rewritten to read deprecations off LspVocabulary.deprecatedCoordinates() (native @deprecated(reason:) for member-level; docstring @deprecated token-scan for whole-directive). New tests: LspVocabularyTest (10 cases pinning structural invariant + deprecation surface), CoordinateAtTest (cursor-to-coordinate across flat / single-level / multi-level nesting), ArgNameCompletionsTest (5 cases), DriftDetectionTest; consumer tests gain @sourceRow regression-guard cases. Self-review (04daf62) lifted siblingStringAt to LspVocabulary (~120 duplicated lines retire from MethodCompletions / Hovers / Diagnostics) and filed R123 for the MethodNameBinding + METHOD_VALIDATING_DIRECTIVES enclosing-directive-context smell that surfaced when the consumers stabilised. Findings noted at approval (non-blocking): (1) Diagnostics.METHOD_VALIDATING_DIRECTIVES is the renamed shadow of the previous VALIDATE_METHOD set; the spec said this would be replaced by MethodNameBinding arms only being attached where method validation applies, but the structural redesign got deferred to R123 instead of landing here. (2) SdlActions.detectLegacyNameSites calls LspVocabulary.load() on every detection (every code-action request re-parses the SDL + re-runs the structural-invariant loop); the workspace’s vocabulary instance is the once-only one, but the SdlAction.Detector signature doesn’t accept a vocabulary so per-request re-parsing is the only option without a contract change. Code-action requests aren’t on the keystroke hot path and the parse is small (~25 directives), so this is minor, but it contradicts the spec’s "shape, not state, read once" framing. (3) The L1 unit BehaviorTest per-binding-arm suite the spec called out didn’t land as a separate file; arm coverage is folded into the consumer tests, which keeps the assertion shapes but loses the tier-by-tier mapping. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25, all 11 reactor modules SUCCESS, 187 LSP tests pass.

  • Discarded: collapse BatchKeyField validator/emitter redundancy (collapse-tabletargetfield-redundancy, R4): superseded wholesale by R58 Phase G (commit 3dcd3c6), which introduced the orthogonal ConditionJoinReportable capability and collapsed the four unsupportedReason overloads in SplitRowsMethodEmitter to a single capability dispatch + the validator’s matching 4-arm instanceof chain to a single instanceof ConditionJoinReportable check. R4’s success criteria (validator 4 → 1 arms, emitter overloads gone, lock-step compiler-enforced) are all met in current trunk; the implementation diverged from R4’s literal proposal ; the predicate lives on a purpose-built ConditionJoinReportable rather than as a default on BatchKeyField, because ServiceTableField is a BatchKeyField without the condition-join concern, so the narrower capability is the cleaner split. Item file deleted in this transition; the work itself shipped under R58.

  • Surface silent @splitQuery on @record-parent fields as a build warning (classification-vocabulary-followups, R3, 17cc1a9 + 715a439): FieldBuilder.classifyChildFieldOnResultType now emits a BuildWarning via ctx.addWarning(...) at both seams that head into RecordTableField / RecordLookupTableField ; the @sourceRow branch (top of the if (DIR_SOURCE_ROW) block, before SourceRowDirectiveResolver runs) and the regular @record-parent branch (immediately after resolveReturnType confirms ReturnTypeRef.TableBoundReturnType, before path / table-field-components / batch-key rejection guards). Holistic surfacing: an unrelated rejection on the same field (bad lifter signature, unresolvable @reference, FK ambiguity) doesn’t suppress the redundancy advisory. Message names the field coordinate (<ParentType>.<fieldName>) and contains the substring "@splitQuery is redundant on a @record-parent field". Channel and prose form mirror the @table-shadowed-by-@record precedent at TypeBuilder.java:663; no new public API, no marker constant (deferred until R121’s LSP arm earns the second consumer). Closes the long-standing promise at code-generation-triggers.adoc:105. Tests. Five pipeline-tier fixtures in GraphitronSchemaBuilderTest: SPLIT_QUERY_ON_RECORD_PARENT_WARNS_TABLE_FIELD, SPLIT_QUERY_ON_RECORD_PARENT_WARNS_LOOKUP_FIELD, SPLIT_QUERY_WARNS_ALONGSIDE_RECORD_PARENT_REJECTION (regular path; the last asserts UnclassifiedField + warning); SPLIT_QUERY_WARNS_ON_SOURCE_ROW, SPLIT_QUERY_WARNS_ALONGSIDE_SOURCE_ROW_REJECTION (@sourceRow path; the last asserts UnclassifiedField with RejectionKind.AUTHOR_ERROR + warning). Each asserts the classification arm via isInstanceOf(...) plus the warning’s message-substring on schema.warnings(). Out of scope, follow-ups filed: LSP-tier diagnostic for the same warning (R121, where the marker constant earns its keep with a real second consumer); FkJoin.alias dead-storage cleanup (R120); generalising the BuildWarning channel into a WarningKind enum (no fourth producer to justify the lift); rejecting @splitQuery on @record-parent fields (the directive remains classified-but-no-op so existing schemas keep building). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • Add Record1<T> source-shape support alongside Row1<T> on the @service classifier path (emit-record1-keys-instead-of-row1, R61, 742f11bd + 3d01c218 + 4ffdfc02 + 8fc61b95 + bfeae318): developers choose either source shape at the @service source declaration ; Set<Row1<Integer>> (Row surface, no value1()) or Set<Record1<Integer>> (Record surface, with value1()) ; and variant identity tracks the chosen shape so emit sites pattern-match instead of re-deriving. The classifier already routed List<Row<N>> / Set<Row<N>> to RowKeyed / MappedRowKeyed and List<Record<N>> / Set<Record<N>> to RecordKeyed / MappedRecordKeyed; this iteration made each variant’s keyElementType() and javaTypeName() reflect the developer’s choice exactly and threaded that through the emit chain. Model. BatchKey.keyElementType() becomes the single source of truth via a default sealed switch on the root: RowKeyed / MappedRowKeyed / LifterRowKeyed produce RowN<...>; RecordKeyed / MappedRecordKeyed / AccessorKeyedSingle / AccessorKeyedMany produce RecordN<...>. javaTypeName() per-variant routes to a shared containerType(container, shape, cols) helper. Variant rename. AccessorRowKeyed{Single,Many} → AccessorKeyed{Single,Many} (8fc61b95): the Row discriminator was leaking an emit-site detail (which jOOQ-typed local the framework picks for the projected key) into variant identity. There is no developer-supplied source on these arms ; the role is the lift-back into Graphitron scope after a @service / @externalField returning a TableRecord ; so the projection axis no longer encodes in the name. Used by ServiceTableField and RecordTableField; the source-shape constraint lives in javadoc and is enforced by FieldBuilder.deriveBatchKeyFromTypedAccessor. Generators. GeneratorUtils.buildKeyExtraction (ParentKeyed) forks by variant identity: RowKeyed / MappedRowKeyed arms emit DSL.row(((Record) env.getSource()).get(table.col), ...); RecordKeyed / MappedRecordKeyed arms emit ((Record) env.getSource()).into(table.col, ...). buildKeyExtractionWithNullCheck stays RowKeyed-only with a defensive IAE on misroute (single-cardinality @splitQuery on a @table parent is the only caller). buildFkRowKey (RecordParentBatchKey RowKeyed) reads scalar values per parent ResultType (jOOQ TableRecord, jOOQ Record, Java record getter, typed POJO getter) and constructs the RowN<...> via DSL.row(...). New buildAccessorKeySingle / buildAccessorKeyMany emit __elt.into(table.col1, ...) to produce RecordN<...> keys, giving the auto-emitted rows-method’s value<N>() access for the parent VALUES table emission. Parent VALUES emission. SplitRowsMethodEmitter forks two ways: RowN-keyed arms (RowKeyed, LifterRowKeyed) use k.field<N>() (returns the inline-value Field a DSL.row(value, ...)-constructed Row carries); RecordN-keyed accessor arms use DSL.val(k.value<N>()) (extract the scalar; wrap as a bind-parameter Field that typechecks against the inline-i first arg of jOOQ’s DSL.row overload). Without the DSL.val wrap the column-reference Field rendered into the VALUES table at runtime instead of the value. Lift Invariant #10 (bfeae318): the validator’s single-cardinality RecordTableField / RecordLookupTableField rejection (validateRecordParentSingleCardinalityRejected) was a downstream gate stranded by the rows-method router pinning the single-record-per-key arm to AccessorKeyedMany only. The data-fetcher side (buildRecordBasedDataFetcher) already handled single cardinality cleanly via the (dispatch == LOAD_MANY || !isList) -> Record valueType rule, so the only missing wiring was teaching emitsSingleRecordPerKey() to also be true for single-cardinality fields. RecordTableField.emitsSingleRecordPerKey() extends to !returnType().wrapper().isList() || batchKey() instanceof AccessorKeyedMany, RecordLookupTableField adds the missing override mirror, and the validator gate drops. @DependsOnClassifierCheck annotations. Two checks under buildAccessorKeySingle / Many describing the Field-typed into(...) projection (accessor-rowkey-shape-resolved from R60); paired with @LoadBearingClassifierCheck on FieldBuilder.deriveBatchKeyFromTypedAccessor. Tests. L1 BatchKeyTest parameterised case pins keyElementType() and javaTypeName() per variant: RowKeyed, MappedRowKeyed, LifterRowKeyed → RowN<...>; RecordKeyed, MappedRecordKeyed, AccessorKeyedSingle, AccessorKeyedMany → RecordN<...>. L3 ServiceFieldValidationTest adds dual-shape cells (MappedRowKeyed / MappedRecordKeyed both classify cleanly on the same field). L3 RecordTableFieldValidationTest / RecordLookupTableFieldValidationTest flip the three SINGLE_* cases from rejection to acceptance. L3 GraphitronSchemaBuilderTest adds RECORD_TABLE_FIELD_SINGLE_CARDINALITY pinning post-R61 acceptance + emitsSingleRecordPerKey() projection. L4 TypeFetcherGeneratorTest.serviceField_mappedRecord_list_keyTypeIsRecordN pins the Set<Record1<Integer>> parameter shape and record.into(...) extraction. L5 TestServiceStub.java keeps both Row1-source and Record1-source fixtures (getFilmsWithSetOfRow1Sources / getFilmsWithSetOfRecord1Sources siblings) as the dual-shape coverage anchor. L6 FilmService.titleUppercase(Set<Record1<Integer>>) -> Map<Record1<Integer>, String> confirms value1() works in the developer-side iteration; the existing Row1-source sibling confirms field1()-based dispatch keeps working. The AccessorKeyedSingle execution path is restored end-to-end against PostgreSQL (FilmCardData(FilmRecord film) + film: Film). Out of scope, deferred: @batchKeyLifter lifter return-type symmetry (the consumer-supplied static method still pinned to org.jooq.Row1..Row22) ; owned by R71. Element-shape conversion for Set<TableRecord> / List<TableRecord> developer signatures ; closed by R70 by extending the variant taxonomy rather than threading conversion through the emitter. Open question closed in this iteration: "does Row1 afford a tuple-IN planner hint that Record1 may not?" Resolution: no. jOOQ’s Record1<T> extends Row1<T>, so every typed Row1-API call site continues to type-check when handed a Record1<T>; framework WHERE-clause emission reads keys via Row-typed APIs and is shape-agnostic. Approval addendum: post-landing drift survives intact (this approval pass on 2026-05-09, ~5 days after In Review). The variant-identity-tracks-shape contract was the foundation two follow-on items explicitly built on. R70 (ea44908f) added TableRecordKeyed / MappedTableRecordKeyed permits to extend the cross-product to a third element shape (developer’s typed TableRecord subtype); keyElementType()’s switch grew two cases, `buildKeyExtraction grew a third arm emitting ((Record) env.getSource()).into(Tables.X). R110 (75379091 + 3992f51 + 8922092 + 3b7f432) replaced @batchKeyLifter with @sourceRow and split LifterRowKeyed into LifterLeafKeyed + LifterPathKeyed under a new LifterKeyed sub-seal of RecordParentBatchKey. R61’s row-vs-record symmetry survived: the lifter arms still produce RowN, the accessor arms still produce RecordN, and the deferred R71 surface (lifter return-type symmetry) remains the only consumer-supplied surface without shape-symmetry. R102, R77, R82, R78, R104, R114 all touched the variant taxonomy or its consumers without disturbing R61’s invariants. Editorial drift in the spec at approval (not blocking, file deleted on Done): the spec body retained pre-R110 names (LifterRowKeyed, @batchKeyLifter) at three call sites; this is purely cosmetic on the now-deleted artifact, and the implementation reflects the post-R110 shape correctly. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25; current trunk passes the same test surface that landed at In Review.

  • Replace @batchKeyLifter with @sourceRow composing with @reference (sourcerow-replaces-batchkeylifter, R110, 7537909 + 3992f51 + 8922092 + 3b7f432): @batchKeyLifter is removed; @sourceRow(className, method) replaces it with flat args (no ExternalCodeReference wrapper, no targetColumns) and composes with @reference so multi-hop paths from a non-table-backed @record parent become expressible. Model. BatchKey.LifterRowKeyed splits into LifterLeafKeyed(JoinStep.LiftedHop hop, LifterRef lifter) (no-@reference leaf-PK arm) and LifterPathKeyed(List<JoinStep> path, LifterRef lifter) (@reference-composed FK chain) under a new BatchKey.LifterKeyed sub-seal of RecordParentBatchKey. LifterPathKeyed’s compact constructor enforces non-empty path. The seal is honest about what it carries today ; the resolver’s typed return narrows to `LifterKeyed, and GeneratorUtils.buildRecordParentKeyExtraction’s exhaustive switch collapses both permits onto a single `case BatchKey.LifterKeyed lk arm because the lifter emit shape (Lifter.method((Backing) env.getSource())) is identical for both ; and what it doesn’t (the SplitRowsMethodEmitter prelude consumes both shapes via the JoinStep.WithTarget capability and RecordParentBatchKey.preludeKeyColumns(), not a LifterKeyed-typed parameter, so the capability-uniformity claim is future-facing rather than load-bearing in production). Resolver. New SourceRowDirectiveResolver with sealed builder-internal Derivation.{Leaf | Path} typing the parent-side tuple source: Leaf reads the leaf target’s PK columns directly (single column-equality JOIN); Path delegates to BuildContext.parsePath(startSqlTableName=null) and takes the first FK hop’s source-side columns. Two diagnostic templates distinguish the two cases (per-position prose names "first-hop source-side column of FK '<fk>'" vs "primary key column '<col>' of '<leaf>'"). @reference parse failures surface directly without re-validating against the lifter. Reflection performs class load, single-static-method discovery, parameter-assignability check against the parent backing class, and org.jooq.Row1..Row22 raw-return + arity-bounds checks before the per-position erasure loop. Three classifier-check keys (@LoadBearingClassifierCheck on the resolver, paired with @DependsOnClassifierCheck on consumers): sourcerow-classifies-as-record-table-field (resolver always projects into RecordTableField or RecordLookupTableField, paired with SplitRowsMethodEmitter.emitParentInputAndFkChain); sourcerow-leafkey-batchkey-is-lifterleafkeyed and sourcerow-pathkey-batchkey-is-lifterpathkeyed (the no-reference vs reference-composed permit guarantee, both paired with GeneratorUtils.buildLifterRowKey). Two keys not one because the relaxation surface is independent: a future variant could allow LifterKeyed outside the leaf-PK / path-keyed split without affecting the other guarantee. LoadBearingGuaranteeAuditTest picks up all three pairs automatically. Tests. L1 unit BatchKeyTest adds lifterPathKeyed_emptyPath_throwsIllegalArgument (compact-constructor invariant) and extends recordParentBatchKeyExhaustiveSwitchCompilesAcrossPermits to verify the sub-seal compiles to a single LifterKeyed arm. L4 pipeline GraphitronSchemaBuilderTest.SourceRowClassificationCase (renamed from BatchKeyLifterCase) carries 16 cases covering: pojo + Row1<Integer> + @reference happy path; lookup-key co-presence; null-fqClassName / @table / jOOQ-record parent rejects; JavaRecordType admit; missing class / missing method / multiple matches / wrong return / wrong param type rejects; arity / column-class / wildcard mismatches; @reference parse failure passthrough; @asConnection reject; @field(name:) non-interaction; field-level @condition co-presence; @orderBy co-presence; scalar return reject; LEAF_PK_NO_REFERENCE (the new no-@reference arm produces LifterLeafKeyed); LEAF_PK_ARITY_MISMATCH (leaf-PK diagnostic distinguishes from path-keyed). All 1465 graphitron tests pass. Sakila fixtures. CreateFilmPayload.language migrated to leaf-PK (@sourceRow alone, no @reference). New Story 1 fixture: Query.customerAddressSummary(customerId: Int!): CustomerAddressSummary whose address field carries @sourceRow + @reference(path: [{key: "customer_address_id_fkey"}]) ; the canonical path-keyed shape. Backed by CustomerAddressSummary Java record, CustomerAddressSummaryLifter.addressIdOf(parent) -> Row1<Integer>, and CustomerAddressSummaryService. End-to-end coverage flows through the L5 compile-spec tier on graphitron-sakila-example. Documentation. New how-to docs/manual/how-to/source-row.adoc (leaf-PK + path-keyed shapes with full SDL + Java + rejection-message anchors). Renamed reference directives/batchKeyLifter.adoc → directives/sourceRow.adoc rewritten for the flat-args directive. Sweep across external-code.adoc (drops @sourceRow from the ExternalCodeReference table; cross-link to the dedicated how-to), result-types.adoc, record.adoc, notGenerated.adoc, condition.adoc, handle-services.adoc, and the reference / how-to indexes. Internal rewrite-design-principles.adoc updated for the sub-seal and renamed classifier keys. Architect-review revisions (3b7f432) tightened the LifterKeyed Javadoc to describe today vs. tomorrow without overclaiming, dropped uninstall AsciiDoc tag markers from schema.graphqls (the docs build is plain Asciidoctor with no example resolver, so tag::sourcerow-leafpk[] / tag::sourcerow-story-1[] had no consumer), and filed R116 (composite-key-row2-source-row-coverage) for the composite-key Row2 path-keyed coverage gap (the resolver admits Row2..Row22 today; the gap is in the test catalog, no 2-column FK exists). Findings noted at approval (non-blocking, follow-ups not yet filed): (1) the LSP module’s hand-written directive registry was not updated for R110 ; graphitron-lsp/.../parsing/DirectiveDefinitions.java:77-80 still defines batchKeyLifter with the obsolete lifter: ExternalCodeReference + targetColumns: [String!]! shape (no sourceRow entry), .../diagnostics/Diagnostics.java:45 lists "batchKeyLifter" not "sourceRow" in VALIDATE_METHOD, and three test files (DirectiveDefinitionsTest, DiagnosticsTest, ClassNameCompletionsTest) pin @batchKeyLifter as part of the registry surface. The build is green because the LSP tests are self-consistent against their own hand-written registry, but the registry has drifted from directives.graphqls so an IDE consumer will surface "unknown directive" diagnostics on @sourceRow and continue to suggest a removed @batchKeyLifter. The flat-args shape may require extending DirectiveDef/InputTypeBinding to express non-ExternalCodeReference arg shapes, so this is a small standalone follow-up rather than a rework gate; the spec body framed migration as "internal-only" because adoption was minimal, but the LSP is a user-facing consumer of the directive surface that the spec didn’t enumerate. (2) docs/manual/reference/diagnostics-glossary.adoc:103 (the lifter-method entry) describes the wrong lifter shape: it says “methodName:” (the actual arg is method:) and “(Set<Key>) -> Map<Key, Value>” (that’s the @service rows-method shape; @sourceRow’s lifter is `(parent backing class) -> RowN<...> per the resolver and the howto). Looks like the entry was copy-pasted from attempt-service-method (line 94) without retargeting. Both findings are tractable as small standalone Backlog items; neither blocks the architectural surface (sealed sub-seal, three classifier-check keys, single-arm switch collapse) which is sound. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • Demote @asConnection + same-table @nodeId guard from rejection to advisory warn (narrow-asconnection-same-table-nodeid-guard, R113, afedb4b8 + 846f055d + e5358818 + 7d59ffd5): R106 lifted same-table @nodeId args from a QueryLookupTableField lookup to a QueryTableField + BodyParam.In filter on the connection rail; one inherited rejection from the lookup era did not compose. R113 first pass narrowed the rejection to the conjunctive ∃ required same-table @nodeId leaf predicate (pathRequired = outer arg && every nested input wrapper non-null) and collapsed the carrier into a sealed AsConnectionGuard.{None | Required(SameTableHit)}. Production schema (opptak-subgraph’s Query.kompetanseregelverkGittIdV2(ider: [ID!]! @nodeId(typeName: "Kompetanseregelverk")): [Kompetanseregelverk!] @asConnection) deliberately composes that shape to ship a paginated WHERE pk IN (decoded_ids) connection to consumers; the rejection blocked a wire format the producer authored on purpose. Rework demoted the rejection arm to a LOG.warn at FieldBuilder.resolveTableFieldComponents; classification falls through to QueryTableField + FieldWrapper.Connection and the connection emitter ships the expected SQL. With the build break gone the sealed two-arm carrier collapsed further to a single nullable SameTableHit firstRequiredSameTableHit field on NodeIdArgPlan (architect-review tightening ; sealed sub-taxonomy was justified to gate a rejection, not a single warn site). The warn routes through ASCONNECTION_HYGIENE_LOG = LoggerFactory.getLogger(FieldBuilder.class.getName() + ".asConnectionSameTableHygiene"), mirroring the BuildContext.idRefShim precedent: stable category address for log filters and migration tooling, independent of FieldBuilder class organisation. formatAsConnectionSameTableRejection renamed to formatAsConnectionSameTableWarning with advisory rather than directive prose; still names field/leaf/typeName for migration tooling to grep on. Conjunctive ∃-required walk and cycle-protection scoping (add on entry, remove on return so sibling subtrees sharing an input-type subgraph each get visited independently) unchanged from the first pass. Tests: pipeline-tier NodeIdPipelineTest.NodeIdConnectionAdvisoryCase (8 cases, all _ALLOWED; required arg/input field/conjunctive cases assert QueryTableField + FieldWrapper.Connection + BodyParam.In on PK + pagination components, structurally identical to the optional cases R106 already shipped; the carrier flip from rejected→allowed is visible as a rename + assertion-shape change). Unit-tier AsConnectionSameTableWarnFormatTest (one requiredLeaf_emitsWarn_namingFieldLeafAndType case via logback ListAppender on the category logger; pins field/leaf/typeName + every page of @asConnection would equal the input set headline + make 'ids' nullable advisory hint ; the stable bits migration tooling can grep on). Execution-tier GraphQLQueryTest.filmsConnectionByRequiredIds_idsSupplied_paginatesBoundedSet mirrors the production shape (required outer wrapper on a same-table @nodeId list arg composed with @asConnection): three ids supplied with first: 2 returns 2 with hasNextPage=true; page 2 after the cursor returns the remaining 1 with hasNextPage=false. Out of scope: directive-based warn suppression (three silencing routes already exist ; nullable leaf, drop @asConnection, FK-target arg); @LoadBearingClassifierCheck annotation (architect-review verified hygiene-only ; annotating would be inert); FK-target @nodeId + @asConnection (composes today via Resolved.FkTarget.DirectFk → BodyParam.In/Eq/RowIn/RowEq); implicit scalar-ID-arg path (synthesised, not authored); element-level nullability inside an outer-required list (the list is bounded once the outer wrapper is non-null). Editorial follow-up noted at approval (not blocking): stale Javadoc at FieldBuilder.java:258 references NodeIdArgPlan.AsConnectionGuard.Required and "rejection message" ; both removed by the carrier collapse. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • Multi-hop @reference path on @nodeId filter input fields, identity-carrying lift (multi-hop-nodeid-reference-filter, R114, b80594ff + 0efba89f + 0d12cc1): NodeIdLeafResolver.resolveFkJoinPath now accepts @reference(path: [...]) of length ≥ 2 on @nodeId(typeName: T) filter input fields and arguments when every step is a JoinStep.FkJoin and every adjacent pair satisfies the lift predicate (each step’s source-side columns are a positional subset of the previous hop’s target-side columns by SQL name). The terminal hop’s source-side tuple lifts back through the chain to a sub-tuple of the first hop’s source-side columns, on the parent’s own table, positionally aligned with the decoded NodeType keys. The DirectFk vs TranslatedFk decision switches from joinPath.get(0) to joinPath.getLast(); the resolver’s Resolved.FkTarget.DirectFk gains a liftedSourceColumns: List<ColumnRef> slot and the four reference carriers (InputField.{Column,CompositeColumn}ReferenceField, ArgumentRef.ScalarArg.{Column,CompositeColumn}ReferenceArg) gain the matching slot, populated at carrier construction. Emitters at FieldBuilder.projectFilters and FieldBuilder.walkInputFieldConditions swap ((JoinStep.FkJoin) joinPath().get(0)).sourceSideColumns() for liftedSourceColumns() read from the carrier; the emitted SQL is the same direct row predicate (field.eq/in(...) for arity 1, DSL.row(...).eq/.in(...) for arity ≥ 2) single-hop direct-FK already produces. Chain length is purely a classifier-time concept; the runtime touches one table, no JOIN, no subquery. Multi-hop is always explicit: the auto-discovery fallback (JooqCatalog.findUniqueFkToTable) stays single-hop only, so disambiguation among A → ? → C chains is the author’s responsibility via per-hop { key: ... }. Two distinct @LoadBearingClassifierCheck keys (not one widened key): nodeid-fk.direct-fk-keys-match widens to "the terminal hop’s target-side columns positionally match NodeType key columns"; new nodeid-fk.identity-carrying-lift covers "every intermediate hop satisfies the lift predicate so the lifted tuple is well-defined and lives on the parent’s own table". Independent invariants for independent future relaxations. Diagnostics are anchored on static final String LIFT_FAILURE_MARKER = "identity-carrying FKs" and CONDITION_STEP_MARKER = "must be a foreign key" constants on NodeIdLeafResolver; tests assert against the constants by name rather than copying prose. Wider migration of R57’s substring-based assertions to constant markers filed as a sibling. Tests: unit-tier NodeIdLeafResolverTest (3 new cases ; multiHopIdentityCarryingLift_succeeds pins the lifted tuple shape on the level_a/b/c chain, multiHopLiftTranslationRejected anchors on LIFT_FAILURE_MARKER, multiHopConditionStepRejected anchors on CONDITION_STEP_MARKER); pipeline-tier NodeIdPipelineTest.{ArgumentFkTargetNodeIdCase.MULTI_HOP_IDENTITY_CARRYING, InputFieldFkTargetNodeIdCase.MULTI_HOP_IDENTITY_CARRYING_INPUT} pin the carrier-side identity (joinPath.size() == 2, BodyParam.RowIn.columns() SQL names = (k1, k2), decode method = decodeLevelA); pipeline-tier QueryConditionsPipelineTest.multiHopIdentityCarryingLift_emitsHelperOnLiftedTuple pins helper-method emission (body-string assertions banned per the test-tier rules; the L3 BodyParam-level case pins the SQL-shape lift). Compilation-tier coverage rides on mvn install -Plocal-db’s `graphitron-sakila-example compile (the lifted-tuple type aligns with decode<TypeName> helper signatures). New nodeidfixture chain: level_a (PK (k1, k2)), level_b (PK (s, k1, k2), FK to level_a on (k1, k2)), level_c (PK (c, s, k1, k2), FK to level_b on (s, k1, k2)), plus lift_fail_{a,b,c} for the translation-failure case; both metadata-registered in NodeIdFixtureGenerator. Howto article at docs/manual/how-to/multi-hop-nodeid-filter.adoc (mental-model first, worked example, two rejection-message sections anchored on the marker constants); SDL is inlined inside the article because the nodeidfixture jOOQ classes live in a separate package from the sakila-example’s jooqPackage (tag:: switch lands with the L6 wiring follow-on). Honest deviations carried forward: the L6 execution-tier round-trip (GraphQLQueryTest.multiHopReferenceFilter_returnsRows, asserting "single-table FROM, no subquery" via ExecuteListener) is deferred to a Backlog sibling because wiring nodeidfixture.level_* into the example needs either a second graphitron-codegen execution or duplicated tables under the sakila-example’s public schema, neither of which is in scope for the carrier-shape change R114 owns. The same precedent already applied to R50’s parent_node/child_ref fixture. Out of scope, follow-ups filed: non-identity-carrying multi-hop @reference on @nodeId (EXISTS-subquery / JOIN-with-translation emission, symmetric to R57’s single-hop translated FK case); rename column / columns slot on the four reference carriers to a role-explicit name (the slot holds NodeType key columns on the target table but reads as "the predicate column"); diagnostic-anchoring policy migration of R57’s substring-based assertions; L6 execution-tier round-trip; Resolved.FkTarget.DirectFk.fkSourceColumns vestigial slot (now fully covered by liftedSourceColumns). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25; LoadBearingGuaranteeAuditTest picks up the new key’s producer/consumer pairs automatically.

  • Make graphitron-maven-plugin IT self-contained via extraArtifacts (maven-invoker-it-extra-artifacts, R111, 11f276184): the two ITs under graphitron-maven-plugin/src/it/ (basic-generate, missing-schema-inputs) failed in Rewrite reactor CI because the forked child Maven could not resolve no.sikt:graphitron-sakila-db:10-SNAPSHOT; CI runs verify, which never installs sibling reactor modules into ~/.m2, and invoker:install only seeds the IT local-repo with the project under test plus its declared dependency tree. Add <extraArtifacts><extraArtifact>no.sikt:graphitron-sakila-db:${project.version}</extraArtifact></extraArtifacts> to the maven-invoker-plugin configuration in graphitron-maven-plugin/pom.xml so the IT’s missing sibling rides the same reactor-cache resolution invoker:install already uses, and rewrite the contract comment in src/it/settings.xml to name <extraArtifacts> as the seam for sibling-module IT deps so the next contributor adds an entry there rather than reintroducing an implicit mvn install prerequisite. Tests: no new IT ; adding a third invoker IT to lock the seam down would mean inventing a synthetic sibling-module dependency; the rewritten settings.xml comment serves the doc-of-record role, and the Rewrite reactor CI workflow is the regression rail. Verification: with the cached snapshot wiped (rm -rf ~/.m2/repository/no/sikt/graphitron-sakila-db), mvn -f graphitron-rewrite/pom.xml verify -Plocal-db --batch-mode runs both ITs to SUCCESS where they previously failed with Could not find artifact …graphitron-sakila-db:jar:10-SNAPSHOT. Out of scope: switching CI from verify to install (would mask the issue and pollute the runner cache); profile-gating the entry (unconditional and harmless either way); auditing other reactor modules for similar issues (graphitron-maven-plugin is the only IT-housing module today). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • @record-parent multi-table polymorphic ChildField classifier arm (record-parent-multitable-polymorphic-classifier-arm, R105, 76c3262f + 518ffc70 + c7c3579c + 21af55604): the ReturnTypeRef.PolymorphicReturnType arm of FieldBuilder.classifyChildFieldOnResultType is no longer a blanket Rejection.deferred("@record type returning a polymorphic type is not yet supported", ""). Three of the four BatchKey.RecordParentBatchKey permits become reachable on @record-backed parents: RowKeyed when the parent is a JooqTableRecordType (hub = parent’s mapped table); AccessorKeyedMany when the parent is a PojoResultType / JavaRecordType exposing a unique zero-arg List<X> / Set<X>-returning accessor for some concrete X extends TableRecord (hub = accessor’s element-Record table). AccessorKeyedSingle is structurally derivable but deferred at the classifier (Rejection.deferred("polymorphic-child-record-parent-single-cardinality")): MultiTablePolymorphicEmitter.buildScalarPerParentFetcher reads parent context as Record parentRecord = (Record) env.getSource() and has no @record-Pojo arm, so producing the permit there would generate code that ClassCastExceptions at runtime on a Pojo source. LifterRowKeyed for polymorphic returns stays deferred per Out of scope (@batchKeyLifter’s `targetTable derivation reads the field’s @table element type, which doesn’t apply to polymorphic returns). The hub TableRef is consumed at classification time (handed to resolveChildPolymorphicJoinPaths for per-participant FK auto-discovery) and never re-read after the field record is constructed, so it stays a classifier-internal local rather than a slot on the field record. New builder-internal sealed result PolymorphicRecordParentResolution.{Resolved(parentKey, hubTable) | Rejected(rejection)} per the principles' "Builder-step results are sealed" rule. New shared private helper collectAccessorMatches factored out of deriveBatchKeyFromTypedAccessor; deriveBatchKeyFromHubAccessor is the polymorphic-callsite sibling whose reduction step discovers the hub from the unique resolvable accessor rather than pinning against an external @table (none on a polymorphic return). The multitable-polymorphic-child.parent-key-extraction-is-batchkey-driven @LoadBearingClassifierCheck splits per-producer (…-table-backed on classifyObjectReturnChildField, …-record-parent on classifyChildFieldOnResultType) per the audit’s one-producer-per-key rule; the two MultiTablePolymorphicEmitter consumer call sites (buildBatchedConnectionFetcher, buildBatchedListFetcher) gain a second @DependsOnClassifierCheck for the new key via the repeatable annotation. New accessor-rowkey-shape-resolved-against-hub key on deriveBatchKeyFromHubAccessor: same-shape sibling of the existing accessor-rowkey-shape-resolved but the identity contract is hub discovery rather than expected-table match. validateChildMultiTableParentPk (GraphitronSchemaValidator.java:347) drops the TableBackedType-gated early-return and reads field.parentKey().preludeKeyColumns() uniformly across all four RecordParentBatchKey permits; signature loses parentTypeName, Map<String, GraphitronType> types and gains the field reference, with both call sites updated. The non-empty invariant moves entirely upstream: RowKeyed’s canonical constructor and `JoinStep.LiftedHop’s constructor both reject empty key columns at construction time, and the classifier routes empty-PK / unresolved-hub parents through `UnclassifiedField. Architect-review type-system tightening: ChildField.{InterfaceField, UnionField} canonical constructors enforce non-null parentKey and parentResultType via Objects.requireNonNull, lifting the validator’s de-facto contract into the type system. Tests: pipeline-tier RecordParentMultiTablePolymorphicPipelineTest (new file under graphitron/src/test/java/no/sikt/graphitron/rewrite/) drives the SDL → classifier path for all reachable permits ; childInterfaceField_recordParent_rowKeyed and _typeSpecEqualsTableBacked (parity-pin between the @record-JooqTableRecord producer and the table-backed producer via methodSpec.toString() comparison so any drift across the two construction sites fails fast), _accessorKeyedMany (hub identity off LiftedHop.targetTable(), LOAD_MANY dispatch), _accessorKeyedSingle_deferred (DEFERRED rejection assertion for the Pojo + single-cardinality shape), and recordParentPolymorphic_pojoWithoutMatchingAccessor_classifiesAsUnclassifiedField (three-option AUTHOR_ERROR with hub-author-error tail). UnionField siblings mirror the InterfaceField cases. Validator-tier InterfaceFieldValidationTest extends with rejects_listArm_onAccessorKeyedManyHubArityOver21 (22-column hub PK on AccessorKeyedMany trips the same Row22 cap as the table-backed RowKeyed path); the now-unreachable rejects_connection_onPkLessParent and rejects_listArm_onPkLessParent tests are dropped (empty-PK is unreachable through the canonical constructors). New resultTypeFor(table) test-fixture helper publishes a sentinel JooqTableRecordType for the type-system non-null contract. GraphitronSchemaBuilderTest.NON_ERROR_POLYMORPHIC_FALLS_THROUGH_TO_DEFERRED_REJECTION updated: the Pet union fixture is Pojo-parent + single-cardinality, exactly the new deferred shape, and now lands on DEFERRED rather than the prior AUTHOR_ERROR. Out of scope, follow-ups: widening MultiTablePolymorphicEmitter.buildScalarPerParentFetcher to consume parentKey + parentResultType analogously to the list arm (lifts the AccessorKeyedSingle defer); contract-on-the-field-record audit shape so consumers cite one key rather than the producer-key disjunction; per-participant constraint coverage beyond resolveChildPolymorphicJoinPaths; user-facing @record-with-polymorphic-children documentation. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • Lift same-table @nodeId arg/field to a WHERE pk IN (...) filter, not a lookup (nodeid-lookup-ignores-filter-siblings, R106, 91c3cb892): same-typename @nodeId args on a table-bound query field now classify as QueryTableField with a BodyParam.In / BodyParam.RowIn predicate against the table’s primary key, instead of the implicit promotion to QueryLookupTableField. Sibling filter args (scalar @condition / @field) compose with the @nodeId arg as ordinary BodyParam predicates rather than being silently dropped under the legacy lookup-promotion gate. Three classifier-seam edits in FieldBuilder.java: the Resolved.SameTable arm reads arg.hasAppliedDirective(DIR_LOOKUP_KEY) instead of hard-coding isLookupKey = true (filter is the new default; explicit @lookupKey re-enables the N×M derived-table shape); the blanket @nodeId @lookupKey rejection is gone on the same-table arm and remains on the FK-target arm with a pointed message ("@lookupKey is meaningless on an FK-target @nodeId arg"); classifyQueryField’s lookup-promotion gate drops the `lookupPlan.anyArgSameTable() half and is now purely hasLookupKeyAnywhere(fieldDef). Pulls the same-table @nodeId path onto the same filter rail as FK-target @nodeId (Resolved.FkTarget.DirectFk already lifts to BodyParam.In/Eq/RowIn/RowEq), collapsing two near-identical paths into one and making mixed-shape inputs first-class. Tests: pipeline-tier NodeIdPipelineTest.ArgumentSameTableNodeIdCase migrates 4 cases from QueryLookupTableField / ScalarLookupArg / DecodedRecord to QueryTableField / BodyParam.In / BodyParam.RowIn assertions; new SAME_TABLE_WITH_FILTER_SIBLING case pins the headline composed-with-sibling lift on a composite-PK NodeType; new SAME_TABLE_WITH_EXPLICIT_LOOKUP_KEY case pins that explicit @lookupKey re-enables the lookup shape; new FK_TARGET_LOOKUP_KEY_REJECTED case pins the new FK-target rejection. Execution-tier GraphQLQueryTest.filmsByNodeIdArgWithTitleFilter_composesPkInWithSiblingFilter exercises the lift end-to-end (PK-IN composed with WHERE title = ?); the existing films_filteredByArgNodeId_returnsRowsMatchingDecodedIds test stayed green with its comment refreshed for the new shape. No @LoadBearingClassifierCheck keys touched (audit-inert flip). Out of scope, follow-ups: collapsing NodeIdArgPlan.{anyArgSameTable, anyNestedSameTable, sameTableHit} into a sealed AsConnectionGuard.{None | Hit} carrier (the @asConnection rejection at FieldBuilder.java:403-407 is the only remaining consumer; clean follow-up); the implicit @lookupKey directive walker inputTypeHasLookupKey; FK-target @nodeId (already a filter, no behavior change). Editorial follow-ups noted at approval (not blocking): stale comments at FieldBuilder.java:1105 ("the same-table arm synthesises isLookupKey: true") and BuildContext.java:181 ("same-table lookup vs FK-target filter") describe pre-R106 behavior and want a refresh. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • RC parity audit: classify GraphitronField/Type leaves and ship coverage gaps (rc-parity-audit-leaf-coverage, R104, Phase 1a/1b 14386cfc8, Phase 1a tests 8ddfb272f, Phase 1c 5bab9ca7f, Phase 1d a032c96f3, Phase 2 e0ff1f923, Phase 3 c1fcbdcb2, Phase 4 ba8149a7f, self-review fixes b947098e8, README link acbcd4799, docs-site staging adc63948a, rework 32e10769): funnel every classifier write through named operations that emit a JSONL trace, then ship a DuckDB-backed post-processor that joins the per-module traces with the sealed-permits inventory and roadmap mentions to produce a regenerable per-leaf coverage report and a consumer-facing migration-fragment. BuildContext’s previously bare `types map and GraphitronSchemaBuilder.buildSchema’s bare `fields map become TypeRegistry / FieldRegistry private fields; type and output-field writes route through classify / enrich / demote / synthesize (each carrying a clean prior-entry precondition); the input-field path routes through FieldRegistry.classifyInput (trace-only ; input fields are embedded in their parent type, not a central map, and the asymmetry is documented honestly rather than fought). ClassificationTrace is gated on -Dgraphitron.classification.trace=<path>; the new parent-pom -Pleaf-coverage profile sets the property to ${project.build.directory}/leaf-coverage.jsonl per module, with a maven-antrun truncate before the test phase so re-runs don’t append on top of stale records. A JUnit 5 extension auto-registered via META-INF/services plus junit.jupiter.extensions.autodetection.enabled=true tags every record produced inside a test’s lifecycle with the running test class and its tier annotation (resolved through the meta-@Tag on @UnitTier / @PipelineTier / @CompilationTier / @ExecutionTier, with @Tag("cross-cutting") exempted into a separate report column). roadmap-tool leaf-coverage opens an in-memory DuckDB connection, exposes the per-module JSONL files as a view via read_json_auto('graphitron-rewrite/**/target/leaf-coverage.jsonl', union_by_name=true), stages parsed leaves (sealed permits + javadoc intent) and mentions (roadmap simple-name grep) tables, and renders both the internal report at graphitron-rewrite/roadmap/inference-axis-coverage.adoc and a consumer-facing --mode=migration AsciiDoc fragment. directive-support gains a sibling --mode=migration render. The migration guide at docs/manual/how-to/migrating-from-legacy.adoc include::`s both fragments under "Authoritative supported surface". Verify-mode of `roadmap-tool is bound to the verify phase and fails CI when the README or the leaf-coverage report drift; the CI workflow now runs mvn verify -Plocal-db -Pleaf-coverage so the trace files exist for the verify check. Tests: unit-tier TypeRegistryTest (8) and FieldRegistryTest (5) pin precondition contracts; ClassificationTraceTest (5) documents the JSONL framing including ThreadLocal context inheritance and JSON escape; ClassificationTraceContextExtension auto-registers and tags every existing test’s classification records with its tier; LeafCoverageReportTest (5) covers parser hierarchy isolation, intent attribution, the nested-record-vs-sealed-parent worked case (MutationField / DmlTableField), and the roadmap-mention join; DirectiveSupportReportTest extension covers the --mode=migration render. Honest deviations carried forward: the directive-support migration fragment has no verify-mode CI binding in roadmap-tool’s verify phase because that fragment reads graphitron-common/src/main/resources/directives.graphqls (a legacy module the rewrite reactor explicitly does not resolve); it regenerates from the docs build instead. Follow-up filed: R107 captures LeafCoverageReport.parseMentions’s simple-name join sensitivity (any roadmap edit that names a leaf class drifts the report). Out of scope, owned by the triage follow-up: classifying each leaf as Covered / Trivial gap / RC-blocker / Defer and spawning sibling Backlog items per RC-blocker; this item ships the regenerable data the triage will read from. Build green: full `mvn -f graphitron-rewrite/pom.xml install -Plocal-db -Pleaf-coverage on Java 25.

  • Extract ConnectionPromoter from GraphitronSchemaBuilder (extract-connection-promoter, R56, 3f1c9af9 + e94a1a9a): the ~250-line Connection-promotion concern (turning @asConnection carrier fields into proper Connection-typed fields, plus synthesising Connection / Edge / PageInfo entries on ctx.typeRegistry) lifts into a final package-private sibling under graphitron/src/main/java/.../rewrite/. GraphitronSchemaBuilder.java shrinks 670 → 288 lines (well past the spec’s ≤440-line target); the orchestrator retains the two-call sequence (ConnectionPromoter.promote(ctx) → ConnectionPromoter.rebuildAssembledForConnections(...)) but the implementation moves. Pure structural extract-class ; no behaviour change, no sealed Resolved (the spec carved out why: this is a single-step structural transformation with no rejection arms; rejection of malformed @asConnection usage already lives upstream in FieldBuilder.classifyField). The local baseTypeName(GraphQLOutputType) helper migrates as a private static on the new class rather than being reconciled against BuildContext.baseTypeName(GraphQLFieldDefinition) (different signatures, different unwrap semantics; consolidation is a separate decision if it ever matters). Tests: new ConnectionPromoterTest (@UnitTier) exercises promotion directly via the existing GraphitronSchemaBuilder.buildContextForTests seam (the R40 test-only entry point that runs the schema generator + TypeBuilder but stops before field classification) ; eight focused-unit cases (directive-driven bare-list carrier, explicit connectionName:, explicit defaultFirstValue:, structural Connection-typed return enrich-path, SDL-declared @shareable PageInfo flag preservation, two-carrier dedup, return-type already names the Connection emits-no-rewrite, item-nullability propagation) plus a noSynthesisedTypes short-circuit regression on rebuildAssembledForConnections. Existing pipeline-tier coverage (GraphitronSchemaBuilderTest @asConnection cases, ConnectionRegistrationsTest, ConnectionTypeValidationTest) stays green as regression. The DIR_AS_CONNECTION directive-presence assertion in validateDirectiveSchema stays put (out of scope by spec). Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db on Java 25.

  • Batched key extraction for ChildField.UnionField / ChildField.InterfaceField via BatchKey (batch-multitable-polymorphic-child-fetcher, R102, Phases A-D f50f80b1, Phase E d42eeebf, scalar-fetcher cleanup e1e45122, rework 13eefedb, javadoc cleanup at approval): the multi-table polymorphic emitter no longer bypasses GeneratorUtils.buildRecordParentKeyExtraction; both arms (list and connection) read parentKey: BatchKey.RecordParentBatchKey and parentResultType: GraphitronType.ResultType off the field record and delegate to the canonical four-shape × four-permit key-extraction helper. The list arm gains DataLoader-batched fetch (one stage-1 UNION ALL with JOIN parentInput plus one per-typename SELECT, regardless of parent count); AddressOccupantsListBatchingTest pins the canonical sakila customer fanout to exactly 4 statements (down from ~14 pre-R102). BatchKey permits with key-column components now enforce non-emptiness via compact canonical constructors (six direct + three via JoinStep.LiftedHop’s slot-list invariant); `containerType’s dead empty-list fallbacks at `BatchKey.java:124, 133 removed. validateChildConnectionParentPk renamed to validateChildMultiTableParentPk, drops the Connection short-circuit, fires uniform N=21 cap on both arms (the shared parentInput VALUES emitter widens to Row<N+1>, topping out at jOOQ’s Row22); the codegen-time IllegalStateException at MultiTablePolymorphicEmitter.java:682-693 is removed in favor of the validator rejection. New multitable-polymorphic-child.parent-key-extraction-is-batchkey-driven @LoadBearingClassifierCheck ↔ @DependsOnClassifierCheck pair on FieldBuilder.classifyObjectReturnChildField (producer) and the two batched fetcher entry points (consumer). parentTable parameter dropped from every MultiTablePolymorphicEmitter helper signature; the dispatch site at TypeFetcherGenerator.java:436-461 collapses accordingly. Tests: audit-tier BatchKeyTest (parameterised non-empty case per permit + JoinStep.LiftedHop); pipeline-tier TypeFetcherGeneratorTest (list-form DataLoader registration, key-tuple arity for single-PK and composite-PK parents, Interface/Union equivalence pin, buildRecordParentKeyExtraction delegation pin); InterfaceFieldValidationTest and UnionFieldValidationTest mirror empty-PK and >21 arity rejections plus 21-boundary well-formed cases on both list and connection arms; execution-tier AddressOccupantsListBatchingTest against Address.occupants: [AddressOccupant!]!. Spec deviations carried forward: lifting the connection-rows participant single-PK truncation at MultiTablePolymorphicEmitter.java:824 into the validator deferred (existing Query.pagedItems → PagedA/PagedB with composite (k1, k2) PK quietly works on the truncation; promoting it to a hard error would block landing). The wrapper parameter for gating that check on Connection re-lands when the lift does. Out of scope, owned by R105: @record-parent classifier arm; lighting up the LifterRowKeyed / AccessorKeyedSingle / AccessorKeyedMany permits at classification time. R102 ships RowKeyed-only; the slot type accepts the full four-permit RecordParentBatchKey sub-seal so R105 wires that arm in without re-touching the records or the emitter. Build green: graphitron module 1434/1434 on Java 25.

  • @record accessor resolution validated at classify time (record-accessor-validation, R88, 0bcb6ebe + b2d798a9 + e5f2dd2e + 863d90c5): downstream consumers got cannot find symbol: getSakId() from javac on generated fetcher classes when the SDL field name didn’t match an accessor on the @record-backed POJO/Java-record (FetcherEmitter.propertyOrRecordValue synthesised the getter name purely from "get" + capitalize(toCamelCase(columnName)) without consulting the backing class). The fix lifts accessor resolution into the classifier per Classifier guarantees shape emitter assumptions and validator mirrors classifier invariants: new ClassAccessorResolver.resolve runs reflective lookup at Class.forName boundary (added to the reflection-roster at docs/rewrite-design-principles.adoc:29), returning sealed AccessorResolution.{Resolved | Rejected} with Resolved directly sealed over GetterPrefixed(Method) | BareName(Method) | FieldRead(Field). Resolution rules track graphql-java’s PropertyDataFetcher lookup order (get<CamelName> → is<CamelName> for boolean → bare <camelName> → public field), enforce return-type assignability against the SDL field’s resolved Java type, and match either a single DataFetchingEnvironment parameter or per-arg parameters whose types match the SDL arg list. Phase E’s type-system tightening narrows the slot on PropertyField.accessor / RecordField.accessor from AccessorResolution to AccessorResolution.Resolved (still nullable for parents that don’t run reflective resolution: jOOQ-record-backed and null-fqClassName PropertyDataFetcher-fallback parents); FieldBuilder routes Rejected through UnclassifiedField carrying a new Rejection.AuthorError.AccessorMismatch arm whose message() appends the @field(name: "…") override hint inside the typed arm so discrimination rides on the seal rather than a message prefix; FetcherEmitter switches exhaustively over the three Resolved arms with no runtime fallback. The class-accessor-resolver-shape-guarantee @LoadBearingClassifierCheck ↔ @DependsOnClassifierCheck pair documents the contract; the consumer’s reliesOn text now describes a static type guarantee rather than a runtime invariant. Tests: pipeline-tier RecordFieldAccessorValidationTest (10 cases ; three rejection arms each pinning the UnclassifiedField routing structurally + producing actionable validator diagnostics; six positive arms exercising GetterPrefixed, BareName, FieldRead, full-env injection, per-arg injection, override-via-@field(name:)); execution-tier RecordExampleType fixture (all three fields resolve to Resolved, exercising the emit / execute boundary); audit-tier LoadBearingGuaranteeAuditTest picks up the producer/consumer pair automatically. Implementation deviation: per-arg injection at the emitter uses Method.getParameters() for argument names, requiring the backing class to be compiled with -parameters; methodCallExpr throws at emit time with a clear error if absent rather than producing silently-broken code. Out of scope, filed as follow-ups: input-side ctor/setter validation for @record-mapped input types; PropertyDataFetcher fallback removal at the null-fqClassName arm; Lombok / explicit-named accessor extensions; Levenshtein "did you mean" candidates. Build green: full mvn -f graphitron-rewrite/pom.xml install -Plocal-db across all 11 reactor modules on Java 25 (modulo two pre-existing unrelated trunk failures last touched by R79/R82).

  • LSP quick-fix and directive-vocabulary registry for the ExternalCodeReference name → className migration (lsp-externalcodereference-name-migration, R93, Phase 1 19e18b23, Phase 2 5258d16f, Cycle 2 75bee87b): two consumer-facing landings on the LSP. Phase 1 introduced DirectiveDefinitions as the LSP’s directive-vocabulary registry (keyed on directive name; each entry carries (argName, inputType, nestedPath) tuples) and migrated ClassNameCompletions.outerArgOf’s hardcoded three-directive lookup onto the derived view `argsByInputType("ExternalCodeReference"). The five sites that appeared in directives.graphqls but were unwired in the LSP today (@externalField, @enum, @tableMethod, @batchKeyLifter, and the nested ReferenceElement.condition inside @reference(path:)) gained completion + diagnostic surface as a side effect. Phase 2 added the code-action surface: SdlAction primitive (named Detector/Rewrite interfaces, sealed RewriteResult.{Edit | Skip} so the bulk action’s count-by-reason pivot is typed) wrapping a single instantiation for the name → className migration with targets = { Member("ExternalCodeReference", "name") }; three activation points (per-site quick-fix on the cursor, file-scoped bulk action, workspace-scoped bulk action emitting a multi-document WorkspaceEdit directly with no executeCommand indirection); diagnostic stance splits on resolution: legacy-and-resolves stays silent (the build-channel LOG.warn in FieldBuilder.parseExternalRef is the migration-tracking signal), legacy-and-unresolved fires error-severity diagnostic mirroring the build’s ExternalRef.lookupError arm and naming the unresolved name plus the two fixes (namedReferences config or write className: directly). Bidirectional drift protection between SDL and the SdlActions registry: every SdlAction.targets() entry must resolve against directives.graphqls’s deprecation markers (SDL `@deprecated() for member-level; structured javadoc-style @deprecated <reason> token in directive description strings for whole-directive); every marker must be covered by either an SdlAction or the MANUAL_MIGRATION_DEPRECATIONS allow-list (at landing: Member("@asConnection", "connectionName") per-instance semantics, WholeDirective("index") deferred to a future per-call-site rewrite). @index’s description string converts the legacy prose "Deprecated: use `@order(index:) instead" to the structured token form so DeprecationMarkers can pick it up; no semantic change for consumers. CompletionData gained a namedReferences slot (4-arg canonical constructor; 3-arg secondary kept for test fixtures); Workspace.openUris() exposed for the workspace-scoped bulk action. Tests: unit-tier DirectiveDefinitionsTest, SdlActionTest (7 cases), DeprecationMarkersTest (10 cases including bundled directives.graphqls parse), SdlActionDriftTest (4 cases including the at-landing-time canonical-set pin); LSP-tier CodeActionsTest (8 cases, including the cycle-2 sibling-diagnostic regression seam asserting the per-site quick-fix surfaces independently of CodeActionContext.diagnostics), DiagnosticsTest extension (9 cases: one resolves-silent fixture, eight per-site unresolved-error fixtures one per ECR-binding directive, plus a canonical @service message-content assertion naming the unresolved name and pointing at the two fixes), ClassNameCompletionsTest extension (existing three-site cases pass after the migration; five new cases cover the previously unwired sites). 171 LSP tests, 0 failures; full graphitron-rewrite install green on Java 25. Carried follow-ups (not in scope, captured for next SdlAction author): CodeActions.countableNoun ignores its parameter and hardcodes the R93 noun (fine for one action; brittle once a second SdlAction lands); applyAll/countResolvable/countSkipped each iterate matches and re-invoke the rewrite, three full passes per file per request (correctness-clean, just wasteful ; a single partition pass producing (edits, skipCount) together would be cleaner). Out of scope: concrete-FQN suggestions for unresolved name: values (deferred until R90 Phase 3’s static-method index lands); renaming the @externalField directive itself (R54, disjoint SDL surface); automating consumer namedReferences config edits.

  • Breaking: missing-vs-null semantics on single-row DML mutations now PATCH-shaped (bulk-dml-mutations, R77 Phase B/C, 4cb22014e + 38e1d5fa3): single-row INSERT/UPDATE/UPSERT no longer write SQL NULL on every classifier-known column regardless of whether the input map carried the key. Insert-side cells now bind DSL.defaultValue(dataType) when the input omits the key (the column default lands; on NOT NULL columns without a default, this surfaces as a NOT-NULL violation rather than the silent null write); DSL.val(value, dataType) when the input carries the key (explicit null writes SQL NULL via typed null binding). Update-side SET clauses are now built from a runtime if (in.containsKey(name)) { sets.put(...) } walk over tia.setFields(): omitted columns drop out of SET entirely, preserving the existing row’s value (PATCH semantics); explicit-null columns write SQL NULL. The UPSERT update branch shares the same dynamic SET walk over DSL.excluded(col), so an omitted column is no longer overwritten by EXCLUDED.col on conflict (which, paired with the insert-side DEFAULT cell, was silent data loss whenever the column had a default). Migration: callers that relied on the old "always write SQL NULL" behavior for omitted columns must set the field to explicit null in the input map; graphql-java’s argument coercion preserves the absent-vs-null distinction in the resulting Map<String, Object> (Map.containsKey is the dispatch key). The structural pin lives in FetcherPipelineTest.dmlInsertField_*containsKey* / dmlSingleRowUpdateField_emitsDynamicSetWalkOverInKeySet; execution-tier coverage in GraphQLQueryTest.createFilm_omittedFieldUsesColumnDefault, createFilm_explicitNullRaisesError, updateFilm_omittedFieldLeavesColumnAlone_explicitNullWritesNull, upsertFilm_omittedFieldOnInsertBranchUsesColumnDefault, upsertFilm_omittedFieldOnUpdateBranchLeavesColumnAlone. Phase E (44f3a6e0e + afd520e47) extended the same dispatch to the bulk arms across all four verbs and added uniform-shape, no-set-fields-present, and duplicate-lookup-key guards on bulk UPDATE plus the per-arm valueType lift, the centralized empty-list short-circuit, and the inline Postgres-only dialect guard on bulk UPDATE; Phase F (87cfa4814 + 9dd81d093) shipped Sakila execution-tier coverage for the four bulk verbs (DmlBulkMutationsExecutionTest, 18 tests covering bulk INSERT/UPDATE/UPSERT/DELETE projection, missing-vs-null pairs on INSERT/UPDATE/UPSERT, divergent-shape and only-lookup-key rejection paths on bulk UPDATE/UPSERT, duplicate-lookup-key guard on bulk UPDATE, empty-list short-circuit per verb) plus the two single-row only-lookup-key analogues (updateFilm_onlyLookupKeyFields_raisesError, upsertFilm_onlyLookupKeyFields_raisesError in GraphQLQueryTest); Phase G (37622aa75) scrubbed eleven stale mutations.md citations across DmlReturnExpression, FieldBuilder, TypeFetcherGenerator, GraphitronSchemaBuilderTest, and TypeFetcherGeneratorTest. Spec deviation acknowledged at approval: the upsertFilms_doNothingMode_skipsUniformityGuard execution test routed to the pipeline tier (FetcherPipelineTest.dmlUpsertField_bulkInput_doNothingMode_omitsUniformShapeAndSetMapEmits) because PostgreSQL enforces NOT-NULL before evaluating ON CONFLICT, so the doNothing-with-divergent-shapes SQL can’t be exercised against Sakila’s film table; the pipeline-tier substitute pins the structural claim (no firstKeys capture, no setsUpdate walk, no .doUpdate() clause, .onConflict(…).doNothing() chain present, bulk List<Map<?,?>> cast preserved). Closing landing: In Progress → In Review at e9746c7ea; In Review → Done approved on this commit.

  • Path expressions in argMapping (argmapping-path-expressions, R84, b3f85cd–91dd082): the right-hand side of an argMapping entry on @service / @tableMethod / @condition may now be a dot-path that walks into nested input fields (e.g. kvotesporsmal: input.kvotesporsmalId). New sealed PathExpr.{Head | Step} carrier replaces Map<String, String> on ArgBindingMap.byJavaName; ArgBindingMap.of(slotTypes, segmentChains) walks segment chains against the GraphQL schema and populates a per-step liftsList flag so the emitter never re-asks the schema. New Result.PathRejected arm covers structural rejections (walk-through scalar/enum/union/interface; unknown segment with closest-match hint). Multi-segment flat paths route through the existing CallSiteExtraction.NestedInputField machinery; intermediate-list paths route through a new ArgCallEmitter.buildListAwarePathExtraction walker that emits element-wise .stream().map(...).toList() for each liftsList=true segment (one-list-deep and two-list-deep shapes both supported). selection.parseEntries(raw) extracts pure syntax (tokens → segment chains) so R69 can consume it directly. ServiceCatalog.reflectServiceMethod parameter-mismatch hint mentions path expressions on every rejection that prints an argMapping example, and pre-fills a concrete reachable path when exactly one matches the unmatched parameter’s Java type across the field’s slots (7-arg overload threads slot types from ServiceDirectiveResolver). Spec deviations (acknowledged at approval time): Result.PathRejected replaces the spec’s pathError carrier slot since path resolution lives in ArgBindingMap.of and a separate slot would be redundant metadata; the parallel walker in ArgCallEmitter was preferred over augmenting NestedInputField with per-segment liftsList to avoid threading an always-false flag through every R63 site. Deferred: enum/text-map/NodeId leaves combined with intermediate-list paths (no live consumer; emitter rejects the combination with an actionable message). Tests: ArgBindingMapTest (22 cases ; head/step shapes, list-shaped intermediates, scalar walk-through rejection, unknown-segment with candidate hint); ServiceCatalogTest Phase F cases (floor mention, no-args negative, stretch unambiguous-prefilled, ambiguous-fallback, type-mismatch-fallback, 6-arg-overload-fallback); execution-tier GraphQLQueryTest sakila fixtures filmsByPath (one-step), filmsByListPath (one intermediate list), filmsByNestedListPath (two intermediate lists). Build green across all four tiers on Java 25.

  • Sealed resolution outcomes for catalog table/FK lookups (catalog-resolution-sealed-outcomes, R81, c48e532): JooqCatalog.findTable(String) now returns TableResolution.{Resolved | NotInCatalog | Ambiguous(schemas)} and findForeignKeyByName(String) returns ForeignKeyResolution.{Resolved | NotInCatalog}; BuildContext.synthesizeFkJoin returns FkJoinResolution.{Resolved | UnknownTable(name, failure) | UnknownForeignKey(fkName)} so the four Optional<TableRef> and four Optional<FkJoin> rejection sites switch on variant directly instead of fabricating eight distinct "not in catalog" strings. JoinStep.FkJoin.fk is non-null by canonical-constructor enforcement; the redundant String fkName component drops (carriers read fk.sqlName()). Catalog construction asserts every schema in the live jOOQ catalog publishes a generated Tables class, throwing IllegalStateException pointing at <tables>true</tables> on miss; TableEntry.toTableRef is consequently non-Optional. findCandidateSchemasFor deletes (the Ambiguous arm carries the schema list inline); findUnqualifiedTable drops .limit(2) since Ambiguous needs the full list. Diagnostic-builder consolidation: BuildContext.unknownTableRejection switches on TableResolution (Ambiguous → structural with qualified-form suggestions; NotInCatalog → unknownTable with Levenshtein candidates); new sibling unknownForeignKeyRejection covers FK-name misses. ServiceDirectiveResolver.computeExpectedServiceReturnType javadoc drive-by: <jooqPackage> → <schemaPackage> (post-R78 phrasing). 26 new FkJoin(name, null, …) test fixtures migrate to TestFixtures.foreignKeyRef(...) factory. Tests: JooqCatalogMultiSchemaTest extends to 45 cases ; three TableResolution arms by name on the multischema fixture (widget resolved, event ambiguous in both schemas, fabricated names NotInCatalog); both ForeignKeyResolution arms; all three FkJoinResolution arms via synthesizeFkJoin; the construction precondition (static verifyTablesClassPresent helper); both diagnostic builders asserting Rejection.AuthorError.UnknownName shape and AttemptKind.FOREIGN_KEY tagging. The FkJoinResolution.UnknownForeignKey arm is structurally unreachable from current production callers (they pre-resolve the FK via findForeignKey before calling synthesizeFkJoin); the taxonomy still expresses completeness so future call sites must handle the shape, and a unit test asserts the variant constructs correctly. Build green: 1350 graphitron unit/pipeline tests + sakila-example end-to-end on Java 25.

  • FK slot pairing reads the FK’s own keyFields list, not the referenced UK’s own field order (R82 follow-up, b3c17e6): BuildContext.synthesizeFkJoin paired slot[i] positionally from ForeignKey.getFields() and ForeignKey.getKey().getFields(). The first is the FK’s referencing-column list in declaration order; the second is the referenced UniqueKey’s own declaration order ; the two are parallel only when the FK’s referenced-column ordering happens to match the parent PK’s declaration order. For an FK declared as e.g. FOREIGN KEY (fk_b, fk_c, fk_a) REFERENCES parent (pk_b, pk_c, pk_a) against a parent with PRIMARY KEY (pk_a, pk_b, pk_c), jOOQ’s getKey().getFields() returns (pk_a, pk_b, pk_c) while getKeyFields() returns (pk_b, pk_c, pk_a). Zipping getFields() against the former produced silent mis-paired slots ; observable as Field<Long>.eq(Field<String>) compile errors in generated @splitQuery rows-method JOIN ON predicates downstream when the FK column types are heterogeneous. The fix swaps f.getKey().getFields() for f.getKeyFields() (the FK’s own ordered referenced-column list, parallel to getFields() by jOOQ’s contract) at BuildContext.java:654. Why R82 missed this: R82 lifted slot orientation (which side is source vs target) into a structural fact, but inherited the intra-FK column pairing from jOOQ’s parallel-list contract without revisiting which list the second side reads from. The structural successor JoinSlotOrientationTest constructs slots directly via TestFixtures.fkJoin rather than driving them through synthesizeFkJoin against a real jOOQ FK, and the retired body-string regression tests (splitTableField_listRowsMethod_reorderedHeteroFk_pairsBySqlNameAndType, childInterfaceField_connection_reorderedCompositeFk_pairsBySqlNameAndType) that would have caught it were dropped in favour of orientation-only structural coverage. sakila’s FKs all declare referenced columns in PK declaration order, so the compile-tier safety net at graphitron-sakila-example was silent too. Reproducer fixture: nodeidfixture.reordered_pk_parent (PRIMARY KEY (pk_a bigint, pk_b varchar, pk_c varchar)) plus nodeidfixture.reordered_fk_child whose CONSTRAINT reordered_fk_child_parent_fkey FOREIGN KEY (fk_b, fk_c, fk_a) REFERENCES reordered_pk_parent (pk_b, pk_c, pk_a) flips the referenced-column order on the FK side. Heterogeneous types make the regression observable as a Java-class divergence in ColumnRef.columnClass ; expected "java.lang.String" but was "java.lang.Long" at slot 0 (source=pk_a, target=fk_b) and the symmetric mismatch at slot 2. Tests: new SynthesizeFkJoinReorderedKeysTest (graphitron/src/test/java/no/sikt/graphitron/rewrite/) drives synthesizeFkJoin against the new FK and asserts (a) jOOQ’s two accessors actually diverge on this FK so the test cannot go silent if a future jOOQ release folds them, (b) per-slot type pairing holds, (c) sourceSideColumns() and targetSideColumns() iterate the FK’s own list. The test fails pre-fix with the slot-0 message above and passes post-fix. Build green: mvn -f graphitron-rewrite/pom.xml install -P!docs -Plocal-db SUCCESS on Java 25; the fix is one line plus a paired comment naming the trap.

  • FK column pairing lifted into typed slots (fk-column-pairing-typed-slots, R82, 2557bf7 + 82d9313): JoinSlot sealed interface (FkSlot pairs source/target columns; LifterSlot collapses both onto a single column by construction so DataLoader-key-tuple-IS-target-column-tuple is a type fact, not a prose precondition). JoinStep.FkJoin and JoinStep.LiftedHop carry List<JoinSlot>; the WithTarget capability returns Iterable<? extends JoinSlot> from slots() so positional access (.get(i), .getFirst(), .subList(...)) is a compile error at every consumer. BuildContext.synthesizeFkJoin orients each slot at synthesis time so emitter sites read direction-blind (target.<slot.targetSide()>.eq(source.<slot.sourceSide()>)) regardless of which end of the catalog FK each maps to. MultiTablePolymorphicEmitter.matchingParticipantCol retired whole; JoinPathEmitter.emitCorrelationWhere drops its parentHoldsFk parameter and arity-mismatch throw; InlineTableFieldEmitter, InlineLookupTableFieldEmitter, TypeFetcherGenerator retire their parentHoldsFk derivations off cardinality / target-table; SplitRowsMethodEmitter.buildSingleMethod’s if/instanceof FkJoin/LiftedHop block collapses to a single `firstHop.slots() iteration through WithTarget. Producer @LoadBearingClassifierCheck on WithTarget.sourceSideColumns() / key fk-join.slots-oriented-source-and-target paired with @DependsOnClassifierCheck on every migrated reader (FieldBuilder ×3, NodeIdLeafResolver, JoinPathEmitter, TypeFetcherGenerator, MultiTablePolymorphicEmitter ×2). Self-ref deviation from spec: spec promised "no signature change, no caller-supplied hint" on synthesizeFkJoin; the table-name comparison is ambiguous for self-referential FKs (category.parent vs category.children navigate the same FK in opposite directions). The fix threads a selfRefFkOnSource boolean (derived from list-cardinality at the call site) through parsePath → parsePathElement → synthesizeFkJoin, consulted only in the self-ref case; non-self-ref FKs ignore it. Tests: JoinSlotOrientationTest (5 model-tier tests pinning slot orientation + the Iterable<? extends JoinSlot> compile-time ban); TestFixtures.fkJoin / liftedHop helpers convert nine test fixtures from positional pairs to slot pairs; the two body-string regressions added at fdfec353 (splitTableField_listRowsMethod_reorderedHeteroFk_pairsBySqlNameAndType, childInterfaceField_connection_reorderedCompositeFk_pairsBySqlNameAndType) retire in favour of the structural model-tier coverage plus the existing compile-tier check at graphitron-sakila-example; two execution-tier tests (inlineTableField_selfRef_listCardinality_returnsChildren, inlineTableField_selfRef_nonRootCategory_hasNoChildren) caught the self-ref ambiguity. rewrite-design-principles.adoc:228 DTO-parent batching recipe updates to the slot-shaped vocabulary. Follow-ups (not blocking): branchParentFkWhere’s class Javadoc still describes the legacy "FK direction is inferred from the FK’s targetTable" framing ; code is now slot-iterating direction-blind; rewrite to match. `batchedBranchJoinPredicate’s `parentPkCols parameter is dead after the lift (the author flagged this in the Javadoc). The self-ref selfRefFkOnSource threading is a design fork worth revisiting: a post-synthesis slot-orientation hint on JoinStep.FkJoin set by the field classifier, or a self-ref classifier check that rejects ambiguous schemas at validate time, would localise the disambiguation closer to where SDL semantics live.

  • Composite-key NodeId condition args land as typed Row<N> end-to-end (query-conditions-composite-key-rown-call-site, R79, 00ca956 + efcf125 + fc075b8 + 0d59ed7 + 7523497 + a428284): the QueryConditions adapter side now hands the composer a typed Row<N><T1, ..., TN> (or List<Row<N><...>>) instead of erasing to RowN, applying the typed adapter / composer pairs principle (added in aa66c7e). BodyParam.RowEq/RowIn drop the dead javaType slot; TypeConditionsGenerator.rowTypeName builds the parameter type from ColumnRef.columnClass(), replacing DSL.row(new Field<?>[]{...}) with the typed DSL.row(table.c1, ..., table.cN) form. ArgCallEmitter’s inline arity > 1 path uses a Java-17-compatible raw-RecordN` pattern + cast to the typed Row<N><...> (parameterized instanceof patterns are JDK 21+); the cast is unchecked at the type-arg level but sound at runtime since the decoder returns Record<N><T1, ..., TN>. Arity > 22 is rejected upstream as a deferred Rejection.structural in NodeIdLeafResolver.resolve with wording tracking the validateChildConnectionParentPk Row22 precedent. The QueryConditionsGenerator shim layer additionally (a) hoists per-class composite-key NodeId decode chains into private static helpers via a CompositeDecodeHelperRegistry deduplicated by (encoderClass, methodName, mode, list) with naming decode<NodeType>{Row,Rows}{,OrThrow}, (b) lifts shared outer-arg Maps to one local when ≥2 NestedInputField callParams reference the same outer arg, and (c) reduces the noCondition()-and chain to a direct return when only one filter applies. Other call sites (Inline*, SplitRows*, TypeFetcher) keep the inline form by passing no registry. Tests: pipeline-tier QueryConditionsPipelineTest (helper-dedup + scalar/list key separation), unit CompositeDecodeHelperRegistryTest (same-key dedup, SKIP↔THROW separation, scalar↔list separation, per-mode body shape ; six tests), unit QueryConditionsGeneratorLiftTest (≥2-share lifts, single does not, distinct outer args do not, cross-filter counts, camelCase naming ; five tests), NodeIdLeafResolverTest.rejects_whenNodeTypeKeyArityExceeds22 against a synthetic 23-column PK in nodeidfixture, and a sakila compilation-tier regression-guard fixture (filmActorsByCompositeNodeIds + FilmActorCompositeNodeIdFilter) exercising the BodyParam.RowIn → typed Row<N> path against real jOOQ. TypeConditionsGeneratorTest.nodeIdInFilter_compositeColumns_emitsRowInWithUntypedRowN renamed and rewritten to assert on the typed form plus a List<Row2<Integer, Integer>> parameter-type sibling assertion.

  • Replace string-scan helper-emission gate with TypeFetcherEmissionContext (type-fetcher-helper-emission-gate, R80, c36734d + c5506e1): TypeFetcherGenerator.generateTypeSpec previously decided whether to emit the graphitronContext helper by serialising every just-emitted method’s CodeBlock and substring-greping for graphitronContext(env) ; an enumerate-or-scan gate whose latest near-miss had silently dropped ServiceRecordField (the only BatchKeyField that doesn’t extend SqlGeneratingField via TableTargetField). Replaced with a per-class TypeFetcherEmissionContext scratchpad: every emitter that writes a graphitronContext(env) call obtains the CodeBlock through ctx.graphitronContextCall(), which records the dependency on the way out; class assembly drains ctx.isRequested(GRAPHITRON_CONTEXT) and emits the helper accordingly. Threaded ctx through ArgCallEmitter (both buildCallArgs overloads, both buildMethodBackedCallArgs overloads, buildArgExtraction), LookupValuesJoinEmitter.buildFetcherBody, SplitRowsMethodEmitter (entry points + emitParentInputAndFkChain), MultiTablePolymorphicEmitter (emitMethods/emitConnectionMethods overloads + the four private build* statics), and the in-file TypeFetcherGenerator.build* privates. Replaced 11 SQL-context literals (graphitronContext(env).getDslContext(env)), the validator pre-step’s getValidator(env), and the multitable tenant-id data-loader name composition with $L interpolation of ctx.graphitronContextCall(). Test impact: graphitronContextHelper_emittedForServiceRecordOnlyClass keeps the helper-presence assertion; the body-string sanity assertion was the test-tier code-string pattern the principles ban, and deletes. Deferred: an architectural review surfaced that the throwaway ctx constructed at three non-Fetchers callers (QueryConditionsGenerator, InlineTableFieldEmitter, InlineLookupTableFieldEmitter) does not record into a context anyone drains, and that @condition(contextArguments: [...]) does reach those callsites (the comment R80 introduced claiming otherwise was wrong). The closing commit c5506e1 corrects the comment; R85 (helper-emission-non-fetcher-hosts) generalises EmissionContext to the Conditions and Type host classes and adds a sakila compile-tier fixture so the path can’t go latent again. Build green: 1308 graphitron unit/pipeline tests on the full mvn -f graphitron-rewrite/pom.xml install -Plocal-db.

  • R68 Phase 1b: tutorial chapter + TutorialSmokeTest (diataxis-user-manual, R68, fa36dbc + d0c63c4): six tutorial pages under docs/manual/tutorial/ plus a @QuarkusTest drift verifier in graphitron-sakila-example. Prose (fa36dbc): 01-prerequisites.adoc (JDK 25, Maven, Docker, git; the docker run -v init.sql:... one-liner; mvn -f graphitron-rewrite/pom.xml install -Plocal-db; cd graphitron-rewrite/graphitron-sakila-example && mvn quarkus:dev; introspection-curl smoke check); 02-first-schema.adoc (the three types Query / Customer / Address and how @table + @field map them to PostgreSQL; honest call-out on @nodeId / @node being out of tutorial scope); 03-first-query.adoc ({ customers { firstName lastName email } } against the live server; the projection-narrowing claim with the rendered SQL shape; the customers(active: true) filter and the @field(name: "ACTIVEBOOL") argument-level mapping that wires it); 04-joining-tables.adoc (single-hop @reference(path: [{key: "customer_address_id_fkey"}]) with the rendered LEFT JOIN; multi-hop storeAddress chain through customer.store_id → store.store_id → store.address_id → address.address_id; the inferred-FK shape on Store.customers); 05-mutations.adoc (the FilmCreateInput @table(name: "film") + @mutation(typeName: INSERT) shape; the RETURNING clause keeping the round-trip count to one; UPDATE via @lookupKey on filmId); 06-going-further.adoc (the four post-tutorial recipes: add-custom-conditions, connections + sort-results, error-channel, test-your-schema, plus pointers into the rest of the manual). tutorial/index.adoc replaced with a real path overview (cross-link to the example module on GitHub, time budget, before-you-start orientation pointer to Quick Start). Two prose divergences from the plan’s worked-example sketch: (1) the closing query uses address { address district } instead of address { addressLine1 } because the example schema doesn’t carry addressLine1; (2) the "going further" page links to connections + sort-results rather than the plan’s earlier pagination-and-sorting because Phase 3 split that recipe into the two shipped pages. Smoke test (d0c63c4): TutorialSmokeTest lives next to the existing GraphqlResourceSmokeTest under graphitron-sakila-example, reusing the SmokeTestPostgresResource QuarkusTestResourceLifecycleManager (Testcontainers Postgres or local-db routing via -Dtest.db.url). Six tests one-per-page-or-query: page1_introspectionVerification ({ __typename } returns "Query"), page3_customersBasicSelection (all five customer first names + Mary’s email present), page3_activeFilter (only the three activebool: true rows), page4_singleHopReference (the address { address district } shape with 47 MySakila Drive), page4_multiHopReference (the storeAddress chain returning both store addresses), page5_createAndUpdateFilm (POST createFilm round-trips a filmId > 5; subsequent updateFilm against that id round-trips the renamed title). @AfterEach DELETE FROM film WHERE film_id > 5 keeps ApprovalQueryExampleTest’s five-film pin honest. Plus a "Use GraphiQL instead of curl" subsection on `01-prerequisites.adoc (the bundled playground at http://localhost:8080/graphiql/) and a one-line nudge in 03-first-query.adoc’s lede so a reader who skipped the GraphiQL intro still notices the option. Plan deviation: the plan’s "Tests" section called for a separate `tutorial-smoke-test Maven module wrapping a shell script around mvn quarkus:dev and curl. The shipped shape is a @QuarkusTest inside the existing graphitron-sakila-example test source. The in-module shape exercises the same JAX-RS endpoint and the same GraphqlEngine / GraphqlResource / AppContext bean wiring that mvn quarkus:dev would, runs naturally inside the mvn verify -Plocal-db invocation that CI already runs from the rewrite reactor (no separate module to wire into the reactor or the docs build), and gives a single-class diff site for future tutorial pages. The drift surfaces the plan named (HTTP endpoint shape, directive existence, query shape) are all covered; "mvn flags" drift is covered by the surrounding rewrite-build itself. Plan markers: Phase 1b heading gains a shipped at fa36dbc + d0c63c4 trailer; the deviation is documented inline on the plan page so a future reader doesn’t go hunting for the absent Maven module. Verification: mvn -f graphitron-rewrite/pom.xml -pl graphitron-sakila-example test -Plocal-db 250/250 (244 prior + 6 new); ApprovalQueryExampleTest continues to pin five films (the smoke test cleans up after itself). The drift-protection seam means a directive disappearing, an endpoint moving, or a generated resolver narrowing differently breaks the corresponding tutorial page before the docs ship.

  • R68 Phase 6: cutover quick-start directive pointer to the new manual (diataxis-user-manual, R68, 863d8be): quick-start.adoc:15 flipped from the legacy graphitron-codegen-parent/graphitron-java-codegen/README.md GitHub pointer to the in-tree xref:manual/reference/directives/index.adoc so readers land on the live, drift-protected directive reference Phase 2 shipped under manual/reference/directives/. The other legacy-codegen-parent mentions in /docs/ are correct as-is and did not move on this commit: fallback notes for features the rewrite stubs (@multitableReference, the polymorphic union pattern), the migration recipe itself (how-to/migrating-from-legacy.adoc), and how-to/index.adoc’s note that the legacy README’s worked examples remain a useful cross-reference until R26 retires the legacy modules. The legacy README itself is out of AI edit scope (CLAUDE.md); the one-paragraph "moved to graphitron.sikt.no/manual/reference/" stub redirect is the Sikt-maintainer companion commit the original Phase 6 body called out, lives outside this plan, and remains the gating step before R26 retires the legacy modules. Plan markers: Phase 6 heading gains a `shipped at 863d8be trailer; the body extends with the as-shipped scope (which legacy mentions moved, which stayed) so a future reader can audit the cutover surface without re-grepping /docs/.

  • R68 Phase 4 second half: explanation chapter (diataxis-user-manual, R68, 1ea0855): six new pages under docs/manual/explanation/ plus an updated index mapping them. Architectural framing: why-database-first.adoc (the database is the source of truth; the GraphQL layer is a typed view; cross-links to graphitron-principles.adoc) and why-jooq-and-graphql-java.adoc (jOOQ is where you work, GraphQL-Java is under the hood, the dependency triple constrains and enables; cross-links to dependencies.adoc). Pipeline framing: how-it-works.adoc (the build-time and request-time paths in 30 seconds, named call-outs for parse/classify/validate/emit and graphql-java/DataFetcher/jOOQ/DataLoader at runtime), classifier-mental-model.adoc (the (parent context, return type) two-axis model in user terms; concrete walk-through of a Customer schema showing same-scope join, @splitQuery-driven batch, and @externalField computed field side by side; the unknown-name-with-candidate-hint and directive-conflict rejection shapes), batching-model.adoc (same-scope joins versus cross-scope batches; @splitQuery opens a new scope keyed by parent PK; @lookupKey parameterises a derived target table; the N × M contract that custom @condition methods must respect; the per-request DataLoaderRegistry lifecycle). "Why does it work that way" answers: design-decisions.adoc covers seven user-visible constraints with their rationale (why @condition methods take a table parameter even when not aliased; why @lookupKey blocks pagination; why mutations require @table on the input type rather than reusing the output’s binding; why selection drives projection; why federation _entities is a @lookupKey shape; why the validator’s diagnostics surface is closed-set; why build-time wins over runtime introspection). All pages cross-link into the directive reference, the diagnostics glossary, the how-to recipes, and (where appropriate) the architecture chapter and the existing top-level explanation pages. No verifier: explanation prose is curated voice, not a surface that drifts mechanically against the code; the directive reference (DirectiveDocCoverageTest), Mojo reference (MojoDocCoverageTest), diagnostics glossary (DiagnosticsDocCoverageTest), and deprecations index (DeprecationsDocCoverageTest) carry the bidirectional drift-protection seams. Plan markers: Phase 4 trailer extended to "Shipped at 868593a (runtime-api), d796c4c (mojo + verifier), <TBD> (explanation)"; explanation half no longer outstanding. AsciiDoctor build green: all six pages render without warnings; full site build succeeds.

  • R68 Phase 4 second half: explanation chapter (diataxis-user-manual, R68, 1ea0855): six new pages under docs/manual/explanation/ plus an updated index mapping them. Architectural framing: why-database-first.adoc (the database is the source of truth; the GraphQL layer is a typed view; cross-links to graphitron-principles.adoc) and why-jooq-and-graphql-java.adoc (jOOQ is where you work, GraphQL-Java is under the hood, the dependency triple constrains and enables; cross-links to dependencies.adoc). Pipeline framing: how-it-works.adoc (the build-time and request-time paths in 30 seconds, named call-outs for parse/classify/validate/emit and graphql-java/DataFetcher/jOOQ/DataLoader at runtime), classifier-mental-model.adoc (the (parent context, return type) two-axis model in user terms; concrete walk-through of a Customer schema showing same-scope join, @splitQuery-driven batch, and @externalField computed field side by side; the unknown-name-with-candidate-hint and directive-conflict rejection shapes), batching-model.adoc (same-scope joins versus cross-scope batches; @splitQuery opens a new scope keyed by parent PK; @lookupKey parameterises a derived target table; the N × M contract that custom @condition methods must respect; the per-request DataLoaderRegistry lifecycle). "Why does it work that way" answers: design-decisions.adoc covers seven user-visible constraints with their rationale (why @condition methods take a table parameter even when not aliased; why @lookupKey blocks pagination; why mutations require @table on the input type rather than reusing the output’s binding; why selection drives projection; why federation _entities is a @lookupKey shape; why the validator’s diagnostics surface is closed-set; why build-time wins over runtime introspection). All pages cross-link into the directive reference, the diagnostics glossary, the how-to recipes, and (where appropriate) the architecture chapter and the existing top-level explanation pages. No verifier: explanation prose is curated voice, not a surface that drifts mechanically against the code; the directive reference (DirectiveDocCoverageTest), Mojo reference (MojoDocCoverageTest), diagnostics glossary (DiagnosticsDocCoverageTest), and deprecations index (DeprecationsDocCoverageTest) carry the bidirectional drift-protection seams. Plan markers: Phase 4 trailer extended to "Shipped at 868593a (runtime-api), d796c4c (mojo + verifier), <TBD> (explanation)"; explanation half no longer outstanding. AsciiDoctor build green: all six pages render without warnings; full site build succeeds.

  • R68 Phase 5 closing slice: deprecations index + DeprecationsDocCoverageTest (diataxis-user-manual, R68, 23c2056): aggregator page reference/deprecations.adoc lists every SDL @deprecated() marker in directives.graphqls (currently @asConnection(connectionName:) and ExternalCodeReference.name) plus the one whole-directive deprecation that the GraphQL spec disallows @deprecated on (@index, the legacy alias for @order(index:)). Each row points at the canonical directive reference page for the migration prose (asConnection.adoc, record.adoc, index.adoc); a separate "Rejected, not deprecated" section calls out @notGenerated so authors don’t conflate the categories. Two honest deviations from the plan are documented inline on the page itself: (1) the source of truth is the SDL @deprecated() marker, not Java @Deprecated annotations on a "directive-classification model" (the legacy classification model lives in the out-of-AI-scope legacy modules); (2) the plan’s "target removal version" column is dropped because the rewrite’s @deprecated(reason:) markers do not carry a structured removal version and there is no separate directive-surface versioning cadence to anchor it on. Verifier: DeprecationsDocCoverageTest extracts qualified <parent>.<member> keys from the SDL by walking backwards from each @deprecated hit to the closest directive @<name> or input <Name> declaration, then asserts every key’s two halves both appear in the doc page (rows naturally include both, e.g. @asConnection(connectionName:) and ExternalCodeReference.name). Whole-directive deprecations are covered via a small WHOLE_DIRECTIVE_DEPRECATIONS allow-list (currently Set.of("index")) that a separate test arm asserts against the doc; adding a new whole-directive deprecation requires adding to both the allow-list and the doc, mirroring the bidirectional drift-protection shape of DirectiveDocCoverageTest. The reference index gains a "Deprecations" section pointing at the new page. Plan markers updated: Phase 5 marked shipped on the heading; Phase 4 gains a "shipped at 868593a (runtime-api) and d796c4c (mojo + verifier)" trailer with explanation pages explicitly noted as outstanding (the second-half Phase 4 slice). Build green: 258 sakila-example tests pass on Java 25.

  • Multi-schema @table rejection: structured ambiguity message + R68 docs delta (R78 follow-up, 41636e9 + 36238b0): closes the "Open follow-ups" line on R78’s changelog entry by landing both the documentation delta R78 deferred and the user-facing rejection R78’s spec quoted but R78 didn’t actually wire. New BuildContext.unknownTableRejection(String sqlName) helper consolidates the @table(name:) rejection-construction decision: branches on JooqCatalog.findCandidateSchemasFor (size >= 2 emits a structural ambiguity message naming the colliding schemas plus inline qualified-form suggestions; otherwise falls through to the existing Rejection.unknownTable with the Levenshtein-ranked candidate hint, covering missing names, qualified misses, and the degenerate single-schema-with-no-Tables-class case). The three @table-directive sites in TypeBuilder (buildTableType, buildTableInterfaceType, buildTableInputType) all route through it, so the better message reaches authors at every directive site that resolves a name: argument. The eight non-@table-directive resolveTable callsites (one in NodeIdLeafResolver, four in BuildContext resolving table names from FK metadata or other classifier-internal sources) keep the existing rejection shape since @table(name:) framing doesn’t fit those error contexts. Docs: reference/directives/table.adoc grows a sentence under name: describing the qualified form and a constraint bullet quoting the new rejection verbatim; how-to/map-types-to-tables.adoc grows a "Tables in non-default schemas" subsection (three SDL snippets covering unique-across-schemas + two qualified-form schemas plus a paragraph on the failure modes); the diataxis-user-manual plan’s "Pending content additions" section flips from "Backlog candidate" to "done" inline. Tests: three new cases in JooqCatalogMultiSchemaTest pin the three branches against the multischema_a/multischema_b fixture (ambiguous unqualified, missing unqualified, qualified miss); construction passes null schema and null ctx since the helper only touches the catalog and BuildContext.buildTypeNamesByTableKey already null-guards. 1307 graphitron + 256 sakila-example + 48 graphitron-maven tests green.

  • Typed jOOQ class references for multi-schema correctness (jooq-multi-schema-typed-references, R78, b334036 + 6add327 + 9e417b5 + 4a61223 + ab64ad0 + 64405d6): fixes a latent multi-schema generated-code bug (imports emitted as <jooqPackage>.tables.X, dropping the schema segment) and a parallel silent-wrong-schema resolution bug (JooqCatalog.findTable picked whichever schema iterated first on a name collision) by replacing string concatenation against String jooqPackage with javapoet ClassName values populated once at parse time from Table<?> reflection. Model: TableRef carries tableClass (the <schemaPackage>.tables.<X> class), recordClass (the <X>Record class via Table.getRecordType()), and constantsClass (the schema’s Tables class) as ClassName; the prior String javaClassName field is gone. New ForeignKeyRef(sqlName, keysClass, constantName) replaces String fkJavaConstant on JoinStep.FkJoin; the Keys host class is the FK-holder schema’s so cross-schema FKs join correctly without per-emitter schema arithmetic. Catalog API: JooqCatalog.TableEntry exposes tableClass(), recordClass(), constantsClass() typed accessors plus toTableRef(sqlName) as the single factory, collapsing BuildContext.resolveTable and ServiceCatalog.buildTableRef (which already returned Optional<TableRef>); findForeignKeyByName(sqlConstraintName): Optional<ForeignKeyRef> replaces fkJavaConstantName(): Optional<String>. Catalog-miss is structural: BuildContext.resolveTable returns Optional<TableRef> and synthesizeFkJoin returns Optional<FkJoin>; consumers route absence through UnclassifiedType / UnclassifiedField rather than empty-string sentinels, so emit sites never see a partial ref. Resolution-time disambiguation: new parseQualifiedTableName(String) plus two findTable shapes (findTable(qualifiedName) and findTable(schemaSqlName, tableSqlName)); unqualified @table(name: "x") resolves iff exactly one schema contains x, collisions reject with "@table(name: 'film') is ambiguous: defined in schemas [public, archive]; qualify as 'public.film' or 'archive.film'", qualified @table(name: "schema.x") scopes to the named schema. The eight directive-parsing sites (three in BuildContext, four in TypeBuilder, one in NodeIdLeafResolver) all participate; directive SDL signature is unchanged. jooqPackage survivors: only JooqCatalog’s constructor (loads `<jooqPackage>.DefaultCatalog reflectively) and CatalogBuilder’s filesystem-path conversion (replace('.', '/')); no emitter takes a `jooqPackage parameter, no concrete + ".tables" / + ".tables.records" / ClassName.get(jooqPackage, "Tables" | "Keys") concatenation survives anywhere in graphitron/src/main/. NodeIdEncoderClassGenerator reads nt.table().constantsClass() per NodeType instead of synthesising one from jooqPackage; QueryNodeFetcherClassGenerator.generate drops a dead jooqPackage parameter; EntityFetcherDispatchClassGenerator and SelectMethodBody switch to entity.table().tableClass(); GeneratorUtils.ResolvedTableNames collapses to a single typeClass field with the other two reading from TableRef; the private buildRowKey, buildAccessorKey{Single,Many}, buildKeyExtraction{,WithNullCheck} helpers become parameter-pure on jooqPackage. Test surface: new multischema_a + multischema_b jOOQ-codegen fixture in graphitron-sakila-db with cross-schema FK (gadget → widget), a shared event table in both schemas, and a unique widget / gadget per schema; JooqCatalogMultiSchemaTest asserts cross-schema tableClass / recordClass FQNs (multischema_a.tables.Widget, multischema_a.tables.records.WidgetRecord), the FK-holder Keys class for cross-schema traversal (multischema_b.Keys), schema-qualified resolution, unqualified-and-unique resolution, and the empty-on-ambiguity policy for the shared event name; TestFixtures.tableRef(...) helper centralises the new ClassName construction across the 107 test sites. Phasing: Phase 1 fixture landed first to turn the compilation tier red; Phase 2 (catalog API + TableRef migration + qualified-name resolver) was the load-bearing change; Phase 3 (ForeignKeyRef + record-class typed refs) and the hotfix sweep finished emit-side cleanup; Phase 4 dropped the dead jooqPackage parameter threading uncovered by the post-migration audit, closing the boundary on both sides (no concatenation in bodies, no parameter on signatures). Open follow-ups: docs delta for the @table(name: "schema.table") syntax (folded into R68’s ## Pending content additions for next-touch-of table.adoc and map-types-to-tables.adoc); execution-tier coverage for cross-schema FK joins at runtime (Backlog stub); LSP directive-validation pass for the dotted syntax (one-line check, not a hard R78 requirement). Build green: mvn -f graphitron-rewrite/pom.xml install -Plocal-db SUCCESS on Java 25.

  • TableRecord-keyed Map returns on @service rows methods (service-rows-tablerecord-key-shape, R70): closes R32’s deferred "element-shape conversion when the developer’s Sources is Set<TableRecord> / List<TableRecord>`" bullet by extending the variant taxonomy rather than threading conversion through the emitter. Two new `BatchKey.ParentKeyed permits ; TableRecordKeyed(parentKeyColumns, elementClass) and MappedTableRecordKeyed(parentKeyColumns, elementClass) ; carry the developer-declared Class<? extends TableRecord<?>> on the variant; keyElementType() returns it directly. ServiceCatalog.classifySourcesType’s `TableRecord element branch reroutes onto the new permits, threading the typed class. Three downstream sites widen their isMapped instanceof checks to include MappedTableRecordKeyed (RowsMethodShape.outerRowsReturnType, TypeFetcherGenerator.buildServiceDataFetcher, TypeFetcherGenerator.buildServiceRowsMethod). GeneratorUtils.buildKeyExtraction’s sealed switch grows one arm emitting `((Record) env.getSource()).into(Tables.X) with Tables.X resolved from the parent table; the rows-method emitter’s return ServiceClass.method(keys) line covers the new variants by construction (the lambda’s keys local is already typed Set<X> / List<X>, the developer’s signature matches, the call type-checks). The deferred-conversion comment on buildServiceRowsMethod drops out. Resolver-side parent-table consistency check: ServiceDirectiveResolver.validateTableRecordSourceParentTable rejects Set<X> against a parent whose record class isn’t X with a candidate-hint pointer; without it the typed into(Tables.X) projection on a wrong-typed parent would silently produce nonsense. New helper BuildContext.recordClassForTypeName(parentTypeName) reads the @table directive on the parent type and looks up the catalog. MappedRowKeyed / RowKeyed docstrings tighten to "only Set<RowN<...>> / List<RowN<...>> classify here"; the variants are now shape-pure, matching RecordKeyed / MappedRecordKeyed siblings. Tests: BatchKeyTest extends the per-variant shape map with the two new permits (keyElementType() returns FilmRecord, javaTypeName() yields java.util.{List,Set}<...FilmRecord>); ServiceCatalogTest’s two existing `TableRecord cells flip from RowKeyed / MappedRowKeyed to TableRecordKeyed / MappedTableRecordKeyed. L5 + L6: new Film.titleTitlecase schema field paired with FilmService.titleTitlecase(Set<FilmRecord>) -> Map<FilmRecord, String> exercises the typed-record path end-to-end against the sakila PostgreSQL fixture; GraphQLQueryTest.films_titleTitlecase_resolvesViaServiceRecordFieldDataLoader_tableRecordSource runs { films { title titleTitlecase } } and asserts each title round-trips through the typed-record extraction. Builds on R61 (variant identity tracks shape; Record.into(Table) projection at the parent-key extraction site, structurally identical to RecordKeyed / MappedRecordKeyed arms but typed to the developer’s element class). Open follow-ups (deferred): single-cardinality typed-record positional signature (X method(X parent) driven by LoaderDispatch.LOAD_ONE) ; confirm if the same TableRecordKeyed permit covers it cleanly when a real schema needs it; custom-scalar V-types in the typed-record map inherit RowsMethodShape.strictPerKeyType’s null-skip arm until R45 lands. Post-landing addition (c6a10133): composite-PK regression-guard cell `ServiceCatalogTest.reflectServiceMethod_compositeKeyTableRecordSources_classifiedAsMappedTableRecordKeyed pinning Set<FilmActorRecord> (2-PK) onto MappedTableRecordKeyed so a future classifier collapse onto MappedRowKeyed is caught at L1 rather than at consumer-build time; mirrors the consumer’s Set<KvotesporsmalRecord> 3-PK shape. Editorial follow-ups noted at approval (not blocking): approval pass on 2026-05-08 (4 days after In Review) confirmed all R70 invariants survive substantial post-landing drift through R102 (BatchKey invariants + record components + validator generalisation), R77 (bulk DML emit), R82 (slot lift), R78 (jOOQ multi-schema typed references ; parentTable extraction now resolves through TableRef.constantsClass()), R104 (TypeRegistry / FieldRegistry), and R114 (multi-hop @reference); the ((Record) env.getSource()).into(Tables.X) extraction continues to compile and run the L6 sakila path. Duplicate-key DataLoader behaviour with TableRecord keys is structurally sound (the mapped DataLoader factory routes through the same hashing path that already works for RecordN keys, and org.jooq.impl.AbstractRecord.equals/hashCode is value-array-based) but the L6 fixture exercises 5 unique films so the de-duplication path is not directly asserted; worth a follow-up sibling if a regression ever surfaces. Build green: mvn install -Plocal-db SUCCESS on Java 25; 1268 unit + pipeline tests pass; 186 GraphQLQueryTest execution tests pass.

  • selection/ parser audit (selection-parser-audit, R30): audit found the parser IS needed. @experimental_constructType(selection: "...") carries a generation-time string argument; graphql-java’s DataFetchingFieldSelectionSet / SelectedField APIs only exist inside a live query execution and cannot substitute. The selection/ package stays; wiring it into the @experimental_constructType classifier is tracked separately.

  • Promote graphitron-test to graphitron-sakila-example (rename, Quarkus runtime, consumer test pattern) (rewrite-example-quarkus-jaxrs, R67, 4c2dc5d + b869b6e + e5314e9 + 4af7001): turns the rewrite’s internal end-to-end test module into a public-facing artifact that doubles as the runnable reference application and the recommended consumer test pattern, closing both the docs gap (docs/quick-start.adoc:21,64 no longer points at the legacy graphitron-example on the retired graphitron-servlet runtime) and the absence of a documented "how do I test my schema" answer for rewrite consumers. Stage 0 (4c2dc5d) splits graphitron-fixtures into graphitron-sakila-db (catalog + jOOQ codegen) and graphitron-sakila-service (Java service fixtures); renames graphitron-test to graphitron-sakila-example; relocates the tier-annotation package (@UnitTier / @PipelineTier / @CompilationTier / @ExecutionTier under no.sikt.graphitron.rewrite.test.tier) from graphitron-fixtures’s main source root into `graphitron’s test source root, republished as a `tests test-jar via maven-jar-plugin’s `test-jar goal so import paths stay stable across the rename; updates every dependent (graphitron, graphitron-lsp, the two graphitron-maven ITs, CLAUDE.md, .claude/web-environment.md, graphitron-rewrite/docs/{README,testing,rewrite-design-principles}.adoc, six javadoc/code comments, SampleQueryService and MutationPayloadLifterTest javadoc). Stage 1 (b869b6e) layers Quarkus 3.34.5 + JAX-RS onto graphitron-sakila-example: imports quarkus-bom, drops the test-scope hibernate-validator + expressly pair for compile-scope quarkus-hibernate-validator, adds quarkus-rest, quarkus-rest-jackson, quarkus-config-yaml, quarkus-jdbc-postgresql, quarkus-agroal, the quarkus-junit5 + rest-assured test pair, and the quarkus-maven-plugin execution. Hand-written runtime under app/: GraphqlEngine (@ApplicationScoped, builds the schema once via Graphitron.buildSchema(b -> {})), GraphqlResource (@Path("/graphql"), POST application/json → application/graphql-response+json per the GraphQL-over-HTTP spec, GET for query-only, fresh DataLoaderRegistry and AppContext per request stashed under GraphitronContext.class on ExecutionInput), AppContext (implements GraphitronContext, per-request DSLContext from the Quarkus-managed AgroalDataSource plus a context-values map fed into getContextArgument). application.yaml configures HTTP port and JDBC datasource via ${VAR:default} env-var defaults pointing at the local-db Postgres. One smoke test (GraphqlResourceSmokeTest + SmokeTestPostgresResource QuarkusTestResourceLifecycleManager) POSTs { customers { firstName } } and asserts 200 + non-empty; in-process query-to-database tests run unchanged alongside it. default-compile pinned to <release>17</release> to keep the consumer-runtime app code under the rewrite’s Java-17 generated-output guarantee. Stage 2 (e5314e9) curates the test surface: 11 existing tests split into src/test/java/.../querydb/ (the four query-to-database tests GraphQLQueryTest, FederationEntitiesDispatchTest, FederationBuildSmokeTest, NoFederationRegressionTest) and internal/ (the seven generator-internal tests). IdempotentWriterTest relocates from graphitron/src/test/java/no/sikt/graphitron/rewrite/ into graphitron-sakila-example/.../internal/ with explicit imports for RewriteContext and GraphQLRewriteGenerator since the package-relative resolution no longer works after the move. Two new worked examples land under querydb/: MatchQueryExampleTest (load .graphql, execute, assert specific paths) + customers_basic.graphql, and ApprovalQueryExampleTest + films_basic.{graphql,approved.json} (execute, serialise to canonical JSON, compare; on divergence write a sibling .actual.json so the next iteration is "diff the two; mv onto approved if intentional"). README.md lands at the module root: opens with the two roles (runnable reference, recommended test pattern), tables which directories to copy for each role, walks through the runtime files, names the two test patterns plus the carve-out for internal/ ("you do not need to copy anything from internal/`"). Stage 3 (4af7001) repoints `docs/quick-start.adoc:21,64 at graphitron-sakila-example; the "Working example" section grows a one-paragraph mention that the same module doubles as the recommended consumer test pattern, with a link to its README; graphitron-rewrite/docs/getting-started.adoc "Hello world" gains a one-line pointer at the example module after the per-request-context worked example. Verification: mvn -f graphitron-rewrite/pom.xml install -Plocal-db builds clean on Java 25 with the example module’s main jar compiled under Java 17; all 1643 tests pass (244 in graphitron-sakila-example); mvn quarkus:dev from the example module boots and serves real Sakila customer rows over HTTP. Unblocks R26 (retire-maven-plugin) on the docs-pointing side and lays the public-facing artifact R68 (diataxis-user-manual) anchors its tutorial chapter and test-your-schema.adoc how-to on. Out of scope and explicitly deferred: deleting legacy graphitron-example/ (R26 owns that gating); HTTP-shaped query-to-database tests (the in-process pattern via graphql-java stays canonical; the smoke test is the only HTTP-shaped check in the module); pedagogical schema simplification (getting-started.adoc remains the on-ramp); test-pattern variants beyond approval + match (richer taxonomies are future follow-ups).

  • Make the typed Rejection hierarchy load-bearing across producers (lift-unclassified-field-onto-sealed-result, R58, 7c10226 + 09541ed + 5d29a3d + 68a062c + 83816e0 + 3dcd3c6): replaces the flat (RejectionKind kind, String reason) pair on UnclassifiedField / UnclassifiedType / ValidationError with the sealed Rejection hierarchy and threads the typed shape from every producer site through every consumer. Top-level AuthorError | InvalidSchema | Deferred, sub-sealed AuthorError.{UnknownName | Structural} and InvalidSchema.{DirectiveConflict | Structural}, StubKey.{VariantClass(@Nullable Class<? extends GraphitronField>) | EmitBlock(EmitBlockReason)}, with a self-contained candidateHint renderer so the model package can render rejection messages without pulling in BuildContext. RejectionKind survives as a derived projection (RejectionKind.of(Rejection)) for the [<kind>] <message> log surface. Phase 0 (7c10226) drops RejectionKind.INTERNAL_INVARIANT; the single producer at FieldBuilder.classifyChildFieldOnTableType’s nested-fields fallthrough becomes an `AssertionError. Phase A (09541ed) introduces the seal and lifts UnclassifiedField to carry Rejection rejection; every classifier Resolved.Rejected arm widens. Phase B (5d29a3d) mirrors the lift onto UnclassifiedType ; 24 sites (21 in TypeBuilder, 3 in EntityResolutionBuilder); three table-resolution sites construct AuthorError.UnknownName via Rejection.unknownTable. Phase C (68a062c) renames TypeFetcherGenerator.NOT_IMPLEMENTED_REASONS to STUBBED_VARIANTS (Map<Class, Rejection.Deferred>); the four SplitRowsMethodEmitter.unsupportedReason overloads collapse onto Optional<Rejection.Deferred> keyed by EmitBlockReason. Phase D (83816e0) walks the direct candidate-hint producers onto typed AuthorError.UnknownName factories (BatchKeyLifterDirectiveResolver, ServiceCatalog via widening ServiceReflectionResult.failureReason: String → rejection: Rejection, FieldBuilder for @nodeId(typeName:) / column-on-FK-resolved-table / scalar-column-miss / DmlKindResult.Unknown); adds factories unknownTypeName, unknownEnumConstant, unknownNodeIdKeyColumn, unknownDmlKind and the leaf-arm prefixedWith(String) instance method (used by the four wrapper sites that thread caller-specific prose onto ServiceReflectionResult.rejection); drops unused AttemptKind.{TABLE_METHOD, ARGUMENT_NAME, FIELD_NAME}. Phases E–I (3dcd3c6): E replaces the nested-rewrap switch in FieldBuilder.classifyChildFieldOnTableType with a single unc.rejection().prefixedWith(parentPrefix) call so the inner variant’s typed components survive the rewrap (an LSP fix-it on a nested column miss no longer has to re-derive candidates by re-running the classifier). F lifts detectChildFieldConflict, detectQueryFieldConflict, and detectTypeDirectiveConflict from String to Rejection.InvalidSchema.DirectiveConflict; migrates explicit conflict sites (@service`@mutation+, `@notGenerated, @asConnection`@splitQuery+, `@asConnection`@lookupKey+` at LookupKeyDirectiveResolver); InvalidSchema.Structural retains 5 classifier-side producers (root invariants, Connection-at-root for @tableMethod, single-cardinality @lookupKey, circular type, @error field shape) so the seal stays valid. G introduces the ConditionJoinReportable capability (unsealed, mirrors BatchKeyField); the four ChildField variants that share the condition-join predicate (SplitTableField, SplitLookupTableField, RecordTableField, RecordLookupTableField) implement it with their per-variant EmitBlockReason and displayLabel; the four unsupportedReason overloads collapse to one capability dispatch and the validator’s 4-arm instanceof chain collapses to a single instanceof ConditionJoinReportable check. H collapses StubKey.None onto a nullable VariantClass.fieldClass (post-Phase-D the inline-Deferred producer set is exactly 3 sites without natural variant-class anchors); the four Rejection.deferred(...) factories collapse to two: deferred(summary, planSlug, fieldClass) and deferred(summary, planSlug). I lifts ValidationError from (RejectionKind kind, String coordinate, String message, SourceLocation location) to (String coordinate, Rejection rejection, SourceLocation location) with kind() and message() projecting from the rejection; all 33 sites in GraphitronSchemaValidator, the 2 sites in GraphitronSchemaBuilder.buildRecipeErrors, and the watch-mode test fixture migrated; validateUnclassifiedField / validateUnclassifiedType / emitDeferredError use prefixedWith to preserve the typed variant under the validator’s per-site prose prefix. Tests: R58TypedRejectionPipelineTest (8 cases) covers the migrated producers end-to-end ; unknownColumn (direct + nested-rewrap survival), unknownTypeName, the unknownServiceMethod four-wrapper prefix path, the directive-conflict cases (@service`@mutation+` and @table`@record+), the `ConditionJoinReportable capability seal, and the validator-side UnknownName survival through prefixedWith onto ValidationError; RejectionRenderingTest extended with 8 model-tier cases for the new factories and prefixedWith preservation across every sealed leaf. Out of scope and tracked separately: deeper carrier widenings whose producers Phase D could not migrate without changing intermediate carriers (ParsedPath.errorMessage, InputFieldResolution.Unresolved.reason, ArgumentRef.ScalarArg.UnboundArg.reason, EnumMappingResolver.EnumValidation.Mismatch joined-prose aggregation, TypeBuilder.keyColumnErrors / failures aggregation) tracked under R66 (rejection-string-carrier-widening); LSP fix-its consuming AuthorError.UnknownName.candidates are R18; threading nested rejection chains as a typed Rejection.NestedReject arm deferred until error-aggregation consumers (LSP, watch-mode) demand it; ArgumentRef.UnclassifiedArg.reason and BuildWarning.message lifts (separate axes, single producers); RejectionKind rename. Build green: mvn -f graphitron-rewrite/pom.xml install -Plocal-db SUCCESS on Java 25.

  • Tighten accessor-derived BatchKey model and emitter coordination (accessor-batchkey-emitter-tightening, R65, b0c6846): six independent architectural cleanups surfaced during the R60 reviewer pass, all landed as one commit. (1) Drop AccessorRowKeyedMany.Container: enum + record component gone; GeneratorUtils.buildAccessorRowKeyMany’s for-loop iterates any `Iterable so the LIST/SET split was never load-bearing. The Set<X> vs List<X> parent-class declaration is still exercised by the two pipeline-tier fixtures (ListPayload, SetPayload); the variant just stops preserving which side it came from. FieldBuilder.AccessorMatch.Many no longer carries the container; BatchKeyTest and the ACCESSOR_ROWKEYED_MANY_*_ACCESSOR pipeline cases dropped the enum-pinning assertions. (2) BatchKeyField#emitsSingleRecordPerKey() capability: new default method on BatchKeyField returning false; overridden on SplitTableField (!returnType().wrapper().isList()) and RecordTableField (batchKey() instanceof AccessorRowKeyedMany). The two consumer sites ; TypeFetcherGenerator’s `scatterSingleByIdx helper-emission gate and SplitRowsMethodEmitter.buildForRecordTable’s `buildSingleMethod routing ; both fold onto the capability, so a future variant whose rows-method emits 1 record per key implements the capability without adding a third disjunct at either site. (3) RecordParentBatchKey#preludeKeyColumns() capability + prelude param tightening: new abstract method; RowKeyed delegates to parentKeyColumns(), the three target-side arms (LifterRowKeyed, AccessorRowKeyedSingle, AccessorRowKeyedMany) delegate to targetKeyColumns() via hop.targetColumns(). SplitRowsMethodEmitter.emitParentInputAndFkChain’s prelude param tightened from `BatchKey to RecordParentBatchKey; the pkCols switch with default -> throw collapsed to batchKey.preludeKeyColumns(). Helper-method chain (buildListMethod / buildSingleMethod / buildConnectionMethod) tightened to match. To carry the chain end-to-end, SplitTableField.batchKey() and SplitLookupTableField.batchKey() tightened from BatchKey.ParentKeyed to BatchKey.RowKeyed (which already implements both ParentKeyed and RecordParentBatchKey); deriveSplitQueryBatchKey return type matches. The two @DependsOnClassifierCheck annotations on the prelude collapsed into one (the JOIN-on side claim about LiftedHop); the BatchKey-side claim is now load-bearing in the type system. TypeClassGenerator.collectBatchKeyColumns’s redundant `instanceof BatchKey.RowKeyed checks became direct accessor reads on stf.batchKey().parentKeyColumns(). (4) Container/element classifier walk lifted into ServiceCatalog: new ServiceCatalog.ContainerKind { SINGLE, LIST, SET } enum + ContainerSplit record + peelContainer(Type, Set<ContainerKind>) helper. classifySourcesType (SOURCES path) accepts LIST | SET; FieldBuilder.classifyAccessorReturn (accessor path) accepts all three. Element-class checking (jOOQ TableRecord subtype, or RowN/RecordN parameterised raw on the SOURCES path) stays per-caller. Both call sites remain inside parse-boundary classes; the shape walk has one home. (5) Typed LoaderDispatch projection: new BatchKey.LoaderDispatch { LOAD_ONE, LOAD_MANY } enum and RecordParentBatchKey#dispatch() accessor; the three single-key arms return LOAD_ONE, AccessorRowKeyedMany returns LOAD_MANY. TypeFetcherGenerator.buildRecordBasedDataFetcher reads batchKey.dispatch() once and forks the loader value type and dispatch call shape on the projection (replacing the inline instanceof AccessorRowKeyedMany). The @DependsOnClassifierCheck annotation on buildRecordBasedDataFetcher was rewritten to reference the dispatch == LOAD_MANY rule; the producer-side description on accessor-rowkey-cardinality-matches-field was tightened in the same pass to drop the obsolete usesLoadMany term. (6) Delete unused ListAccessorOnSingleField fixture: the record had javadoc noting it existed "for symmetry"; no test referenced it. The unused FilmActorRecord import dropped with it. Deviations from spec: (a) Item 5 implemented as enum rather than the spec’s proposed sealed LoaderDispatch { LoadOne | LoadMany } since both arms carry no per-arm data and consumers fork on identity, not on captured fields (per the design principle "When variants carry different data, use a sealed interface; an enum forces every variant to have the same shape" ; both arms share the empty shape). (b) Item 3’s SplitTableField / SplitLookupTableField batchKey() tightening was not explicitly called out by the spec, but proved necessary to type the prelude parameter as RecordParentBatchKey end-to-end (the alternative was a runtime cast at the call site); type-only narrowing, no behavioural change. (c) Items 2 and 5 capabilities kept separate rather than collapsed: emitsSingleRecordPerKey is a BatchKeyField-level question (rows-method shape; depends on field cardinality); dispatch is a RecordParentBatchKey-level question (loader call shape). They coincide for RecordTableField with AccessorRowKeyedMany (both true / LOAD_MANY) but diverge for single-cardinality SplitTableField (emitsSingleRecordPerKey == true, no dispatch projection ; SplitTableField carries RowKeyed whose dispatch() is LOAD_ONE regardless of field cardinality). Verification: mvn -f graphitron-rewrite/pom.xml install -Plocal-db SUCCESS on Java 25; 1262 unit + pipeline tests pass; 238 graphitron-test compilation + execution tier tests pass; LoadBearingGuaranteeAuditTest no orphans. Out of scope (unchanged): renaming AccessorRowKeyedSingle / AccessorRowKeyedMany (names accurately reflect cardinality at the variant level); the Single permit’s emitter wiring (already complete in R60; execution-tier coverage gap tracked under the validator’s Invariant #10 lift); RecordBatchKeyResolution and the AccessorDerivation / AccessorMatch two-stage builder hierarchy (clean applications of "Builder-step results are sealed").

  • @service rows-method body ; strict return-type validation + shape lift to model (service-rows-method-body, R32, 64b8e2c + e28540b + 83bcfdf): closes out R32 by mirroring ServiceCatalog.reflectServiceMethod’s strict-return check on the child `@service path and resolving the (returnType, BatchKey) → Map<K, V> / List<List<V>> / List<V> cross-product once at the model layer. Iteration 1 (the body emission, separately captured below as befc156) shipped earlier; this entry covers iterations 2 and 3 plus a review-pass nit. Iteration 2 ; strict child-@service validation: ServiceDirectiveResolver.validateChildServiceReturnType rejects developer methods whose declared return type doesn’t structurally match the rows-method’s outer shape. Per-key V derives from ReturnTypeRef ; raw org.jooq.Record for TableBoundReturnType, the backing class for ResultReturnType with non-null fqClassName, the standard Java type for the five standard GraphQL scalars (String / Boolean / Int / Float / ID); other cases (custom scalars, enums, PolymorphicReturnType, ResultReturnType with no backing class) skip the strict check. Carries the service-directive-resolver-strict-child-service-return @LoadBearingClassifierCheck key, paired with @DependsOnClassifierCheck on TypeFetcherGenerator.buildServiceRowsMethod; the emitter can now emit return ServiceClass.method(<args>); against a structurally-typed return without a defensive cast or wildcard local. Author errors surface at classify time rather than as javac errors on the generated source. Iteration 3 ; lift rows-method shape onto the model: validator and emitter each used to reconstruct Map<K, V> / List<List<V>> / List<V> from (returnType, batchKey) independently, the per-key V derivation lived a third time on ChildField.ServiceRecordField.elementType() with a deliberately-divergent fallback, and GeneratorUtils.keyElementType had been bumped to public so the classifier-tier validator could import from the generators package. The shared form lives in two new model-package surfaces: BatchKey.keyElementType() (a default accessor on the sealed root, replacing the static helper in GeneratorUtils) and RowsMethodShape.{strictPerKeyType, outerRowsReturnType, standardScalarJavaType} (the per-key V decision and the (isMapped, isList) outer-shape construction). Validator and emitter both call RowsMethodShape.outerRowsReturnType(perKey, returnType, batchKey); only the perKey input differs (validator: RowsMethodShape.strictPerKeyType and skip on null; emitter: the field-known V from the literal RECORD constant or srf.elementType()). The @LoadBearingClassifierCheck / @DependsOnClassifierCheck pair still holds the contract at audit time, but the construction can no longer drift across sites. GeneratorUtils.keyElementType is gone; the class reverts to package-private and the classifier-tier validator no longer imports from the generators package. Review-pass nit (83bcfdf): split the cast from the value extraction in validateChildServiceReturnType so the Param.Sourced filter uses class::isInstance / class::cast and assigns to a typed local before reading .batchKey(). Behaviour-preserving; null-tolerance contract unchanged. Tests: GraphitronSchemaBuilderTest.UnclassifiedFieldCase.CHILD_SERVICE_TABLE_BOUND_WRONG_RETURN_REJECTED (declared LanguageRecord instead of List<Record>) and CHILD_SERVICE_SCALAR_WRONG_VALUE_TYPE_REJECTED (declared Map<Record1<Integer>, Integer> for a String-valued field) pin the two rejection arms; the previously-shipped GraphQLQueryTest.films_titleUppercase_resolvesViaServiceRecordFieldDataLoader continues to exercise the end-to-end positive path against PostgreSQL. The dropped positive cell collapses onto the validator-and-emitter shared RowsMethodShape.outerRowsReturnType call so structural drift between them is no longer reachable. Open follow-ups (deferred or tracked elsewhere): element-shape conversion when the developer’s Sources is Set<TableRecord> / List<TableRecord> (deferred until a real schema needs it; builds on top of R61); the Row1 → Record1 framework switch (R61, emit-record1-keys-instead-of-row1.md); ParamSource.Context’s typed registry (tracked under `typed-context-value-registry.md).

  • Auto-derive BatchKey from typed TableRecord accessor on @record parents (auto-derive-batchkey-from-typed-record-accessor, R60, 14889c1 + aabd7ea + b2ae55d): closes the @record-parent free-form-DTO rejection in FieldBuilder.classifyChildFieldOnResultType for the case where the parent class already exposes a typed zero-arg instance accessor returning a concrete jOOQ TableRecord (single, List<X>, or Set<X>). The classifier reflects on the parent class once at build time, matches accessors by name (literal, get<Ucfirst>, is<Ucfirst>) and shape (X, List<X>, Set<X> for X extends TableRecord whose mapped table equals the field’s @table return), and produces one of two new BatchKey.RecordParentBatchKey permits ; AccessorRowKeyedSingle(JoinStep.LiftedHop, AccessorRef) for single-cardinality fields, AccessorRowKeyedMany(JoinStep.LiftedHop, AccessorRef, Container) for list / set fields ; without requiring the schema author to add @batchKeyLifter. The three-option AUTHOR_ERROR (typed accessor / @batchKeyLifter / typed jOOQ TableRecord) replaces the previous two-option message. Model: new AccessorRef(parentBackingClass, methodName, elementClass) carries pre-resolved javapoet ClassName`s, sibling of `LifterRef; BatchKey.RecordParentBatchKey’s permit list grows from 2 to 4 (still permits `RowKeyed + LifterRowKeyed); both new permits delegate targetKeyColumns() to JoinStep.LiftedHop#targetColumns() so the DataLoader-key column tuple cannot diverge from the JOIN target columns. Builder-internal sealed hierarchy: RecordBatchKeyResolution.{Resolved, Rejected} lifts the per-field resolution into a sealed result the call site exhausts (Principle 8 "Builder-step results are sealed"); per-method AccessorMatch.{Single, Many, CardinalityMismatch} and call-result AccessorDerivation.{Ok, None, Ambiguous, CardinalityMismatch} capture the reflection match and reduction respectively, neither leaking past FieldBuilder (Principle 7 "Builder-internal sealed hierarchies for multi-target classification"). The accessor-arm rewrites the joinPath to [liftedHop] so SplitRowsMethodEmitter’s prelude reads target accessors uniformly through `JoinStep.WithTarget. Emitter: GeneratorUtils.buildRecordParentKeyExtraction’s switch grows from 2 to 4 arms; `buildAccessorRowKeySingle emits ((BackingClass) env.getSource()).<accessor>() followed by DSL.row(__elt.get<Pk>(), ...); buildAccessorRowKeyMany emits a typed for-loop over the accessor’s Iterable return building a List<RowN<...>> for loader.loadMany. TypeFetcherGenerator.buildRecordBasedDataFetcher switches the loader value type to Record (1:1 with element-PK keys) and the dispatch to loader.loadMany(keys, Collections.nCopies(keys.size(), env)) when the BatchKey is AccessorRowKeyedMany; result type still follows the field’s GraphQL cardinality. SplitRowsMethodEmitter.buildForRecordTable routes AccessorRowKeyedMany through buildSingleMethod (1 record per key, scatterSingleByIdx) rather than buildListMethod; buildSingleMethod widens its first-hop cast from JoinStep.FkJoin to JoinStep.WithTarget with a conditional whereFilter lift, since both FkJoin (single-cardinality SplitTableField) and LiftedHop (loadMany-many) reach it. TypeFetcherGenerator.hasSingleSplitField widens to also gate scatterSingleByIdx emission on any RecordTableField carrying AccessorRowKeyedMany. The shared prelude’s pkCols switch in SplitRowsMethodEmitter.emitParentInputAndFkChain admits all four RecordParentBatchKey permits via targetKeyColumns() (lifter / accessor) and parentKeyColumns() (RowKeyed). Load-bearing keys: two new @LoadBearingClassifierCheck keys ; accessor-rowkey-shape-resolved (the producer guarantees the parent backing class, the accessor identity, and the element class are all reflectively confirmed before emitting AccessorRowKeyedSingle / AccessorRowKeyedMany; consumed by buildAccessorRowKeySingle / buildAccessorRowKeyMany which cast env.getSource() and invoke the accessor without instanceof or null guards) and accessor-rowkey-cardinality-matches-field (the producer pairs AccessorRowKeyedMany with list-cardinality fields and AccessorRowKeyedSingle with single-cardinality; consumed by buildRecordBasedDataFetcher’s `usesLoadMany ⇔ valueType = Record rule). LoadBearingGuaranteeAuditTest passes; no orphans. Test surface: unit-tier BatchKeyTest (5 cases, including a four-permit exhaustive-switch compile pin); pipeline-tier AccessorDerivedBatchKeyCase (6 cases ; list × list-accessor / list × set-accessor / single × single-accessor / ambiguous candidates / cardinality-mismatch / heterogeneous element); execution-tier AccessorDerivedBatchKeyTest runs the Many end-to-end, asserting one batched JDBC round-trip across two parents (3 element-PK keys), the (values (0, ?), (1, ?), (2, ?)) shape, and per-parent record redistribution. Deviations from spec: (a) Container enum on AccessorRowKeyedMany is preserved on the model but the for-loop iterates any Iterable, so emit no longer forks on it (acknowledged technical debt; tracked under R65 #1 follow-up); (b) Many rows-method routes through buildSingleMethod rather than a new shape, since loadMany is 1:1 record-per-key by contract; (c) loadMany overload requires List<Object> of key contexts, so the dispatch passes Collections.nCopies(keys.size(), env) and the batch loader reads keyContexts[0] as before; (d) Single permit is fully wired through the emitter but blocked at validate-time by Invariant #10 (single-cardinality RecordTableField rejection), so end-to-end execution-tier coverage waits for that gate to lift. Reviewer-pass follow-up: R65 accessor-batchkey-emitter-tightening filed for six architectural cleanups surfaced during the review (Container slot, hasSingleSplitField predicate union, prelude default -> arm, two-classifier reflection-walk dedup, two-site dispatch fork in buildRecordBasedDataFetcher, unused ListAccessorOnSingleField fixture). Build green: mvn -f graphitron-rewrite/pom.xml install -Plocal-db SUCCESS on Java 25; 1247 unit/pipeline tests + execution tier all pass.

  • Lift @lookupKey partition onto TableInputArg (dml-lookup-key-partition-on-tableinputarg, R62, b4624f4): adds lookupKeyFields and setFields projections to ArgumentRef.InputTypeArg.TableInputArg, populated once via a new TableInputArg.of(...) factory at the two construction sites (FieldBuilder.classifyArgument, MutationInputResolver.resolveInput). The narrow List<InputField.ColumnField> element type expresses the mutation-arm guarantee that DML inputs admit only Direct-extracted ColumnField; query-side TIAs simply contribute zero entries because @lookupKey lands only on a ColumnField. Three consumers drop their ad-hoc Set<String> rebuild: MutationInputResolver Invariant #4 reads setFields().isEmpty(); buildMutationUpdateFetcher walks tia.setFields() (no skip-during-walk, no cast); buildMutationUpsertFetcher walks tia.setFields() for the SET clause and reads !setFields().isEmpty() for the .doUpdate() / .doNothing() dispatch (the col/val lists still walk fields() to keep @lookupKey fields on the insert branch). dml-mutation-shape-guarantees consumer reliesOn strings updated to drop the "skip-the-set-during-walk" phrasing; LoadBearingGuaranteeAuditTest continues to pair the producer (FieldBuilder.buildDmlField) with the four emitter consumers. Pipeline coverage in GraphitronSchemaBuilderTest (UPDATE_TIA_PARTITIONS_FIELDS_INTO_LOOKUP_AND_SET, UPSERT_TIA_PARTITIONS_FIELDS_INTO_LOOKUP_AND_SET) asserts the typed projections land in declaration order; existing UPDATE / UPSERT execution-tier tests pass unchanged. Architectural follow-up to R22, surfaced in 181c28f.

  • Mutation bodies (mutations, R22, b699c5a + d792463 + 4dc4c04 + 2e9712e + 181c28f, plus pre-branch trunk history for Phase 1A / Phase 3 DELETE / Phase 6 service variants and the R50 cleanup pass): lifts all six mutation leaves out of TypeFetcherGenerator.STUBBED_VARIANTS ; MutationField.MutationInsertTableField, MutationUpdateTableField, MutationDeleteTableField, MutationUpsertTableField, MutationServiceTableField, MutationServiceRecordField. Highest-aggregate stub class going in (131 combined production rejections at the start of the work). Phase 1A (model + classifier): shared DmlTableField sealed supertype permits the four DML records, all sharing (parentTypeName, name, location, returnType, tableInputArg, encodeReturn, errorChannel); one FieldBuilder.classifyMutationInput(fieldDef, typeName) helper enforces Invariants #1 through #14 across all four DML verbs; the mutation-arm switch in classifyMutationField builds the appropriate variant record from the resolved tia + encodeReturn. The MutationField.DmlTableField lift and the four DML records actually shipped as part of R50’s cleanup pass (R50 deleted InputField.NodeIdField, NodeIdReferenceField, IdReferenceField, NodeIdInFilterField and folded their cross-table cases under ColumnReferenceField/CompositeColumnField; the post-R50 input-field shape is what the DML emitters consume). Phase 1B (model alignment, b699c5a): replaces the broad (returnType, encodeReturn, payloadAssembly) triple on DmlTableField with a single sealed DmlReturnExpression returnExpression slot. Five arms (EncodedSingle, EncodedList, ProjectedSingle, ProjectedList, Payload) cover exactly Invariant #14’s admitted return-type set; the Payload arm absorbs the R12-introduced Optional<PayloadAssembly>. Records went from 8 components to 6; emitters pattern-match a single sealed dispatch with no instanceof ScalarReturnType / wrapper().isList() / Optional.orElseThrow() / payloadAssembly().isPresent() predicates. New load-bearing key dml-mutation-shape-guarantees annotates FieldBuilder.buildDmlField (producer) and the four DML emitters (consumers); LoadBearingGuaranteeAuditTest enforces the pairing. Phase 2 (INSERT, d792463): buildMutationInsertFetcher plus the verb-neutral buildDmlFetcher skeleton (try/catch envelope, dsl chain, payload bind, returnSyncSuccess/catchArm) and the emitDmlReturnExpression projection terminator extracted from DELETE; column list and parallel values list both walk tia.fields() once, values use DSL.val(in.get(name), Tables.T.COL.getDataType()) for converter-mediated coercion. Execution-tier createFilm_insertsRowAndReturnsProjectedFilm against PostgreSQL verifies RETURNING $fields end-to-end, resolving the verification gap DELETE shipped with. Phase 3 (DELETE): shipped pre-branch on trunk; later retrofitted to the Phase 1B shape via the same buildDmlFetcher skeleton (buildMutationDeleteFetcher pattern-matches on f.returnExpression() via emitDeleteEncoded / emitDeleteProjected / emitDeletePayload helpers, no instanceof predicates). Phase 4 (UPDATE, 4dc4c04): buildMutationUpdateFetcher shares the same skeleton; SET clause walks tia.fields() skipping @lookupKey names, WHERE clause reuses buildLookupWhere. Execution-tier updateFilm_updatesRowAndReturnsProjectedFilm against PostgreSQL inserts a marker row, runs the mutation, asserts the SET clause wrote and RETURNING $fields returned the new title with languageId carrying through unchanged. Phase 5 (UPSERT, 2e9712e): buildMutationUpsertFetcher against the same skeleton; INSERT col/values lists walk tia.fields() once (every field, @lookupKey included), SET clause skips @lookupKey names, .onConflict(<keys>) reads from tia.fieldBindings(). Empty-SET case emits .doNothing() (jOOQ rejects .doUpdate() with no .set calls). UPSERT additionally carries an Oracle-dialect runtime guard (jOOQ silently translates INSERT ... ON CONFLICT to MERGE INTO with semantics drift; jOOQ exposes no setting to disable the emulation). Two execution-tier tests cover both branches. Phase 6 (service mutations): shipped pre-branch on trunk. Both MutationServiceTableField and MutationServiceRecordField un-stubbed by delegating to the shared buildServiceFetcherCommon helper; the R12 §3 try/catch wrapper, §5 Jakarta validation pre-step, and §2c resultAssembly success-arm assembly all carry over for free on the mutation side. Both wear @DependsOnClassifierCheck(key = "service-catalog-strict-service-return", ...). Architectural follow-ups (181c28f): a post-Phase-5 review surfaced two model lifts that don’t gate the stub-lift work but tighten the model the emitters consume; promoted to standalone roadmap items (dml-lookup-key-partition-on-tableinputarg, R62; dml-dialect-requirement-on-model, R63) with the relevant design discussion preserved there. An adjacent finding from the same review (SplitRowsMethodEmitter.unsupportedReason returning Optional<Rejection.Deferred> only to have callers immediately call .message() to feed buildRuntimeStub’s `String parameter, dropping the typed EmitBlockReason) lives in R58’s domain rather than R22’s and is captured as runtime-stub-takes-deferred-rejection (R64). Out of scope and tracked separately: listed inputs (in: [FilmInput]), nested @table inputs (NestingField), @nodeId-typed input fields (NodeIdDecodeKeys-extracted ColumnField), ColumnReferenceField / CompositeColumnField / CompositeColumnReferenceField in mutation inputs (all gated as deferred at classify time), build-time INSERT column-coverage validation (deferred until jOOQ catalog reliably exposes NOT-NULL + default metadata), non-ID/non-TableBoundReturnType return types on DML fields (Int/Boolean/Connection<T> rejected at classify time; an AffectedCount arm on DmlReturnExpression is the future lift if needed), ScalarReturnType(ID) on non-@node tables (rejected with descriptive message), transaction wrapping (caller’s responsibility via dsl), non-PostgreSQL dialects (RETURNING and ON CONFLICT are Postgres-specific; UPSERT additionally carries the Oracle runtime guard), @mutation + @service mutual-exclusion (already rejected at classifier time). Build green: mvn -f graphitron-rewrite/pom.xml install -Plocal-db SUCCESS on Java 25.

  • Sharpen author-error messages with concrete remediations (sharpen-author-error-messages, R59, 14a5cce + 7003aa2 + cb20a25 + ce43469): four validator rejection messages gain concrete fix suggestions instead of stopping at the diagnosis. ServiceCatalog parameter-mismatch (ServiceCatalog.java:233-254): branches on zero-arg / one-arg / many-arg cases, pre-fills the actual sole arg name, and offers two remedies (rename the Java parameter, or bind via argMapping: "<javaParam>: <graphqlArg>" on @service); the empty-args branch suggests removing the parameter, adding a GraphQL argument, or registering a context key. MutationInputResolver listed-@table-input rejection (MutationInputResolver.java:249-255): names the supported single-non-list @table input wrapper shape and points at the bulk-mutation roadmap gap. FieldBuilder payload-multi-ctor rejection (FieldBuilder.java:1486-1492 and :1600-1606): lists found ctor signatures via the shared formatCtorSignatures helper and suggests record conversion or removing extras. BuildContext zero-FK / multi-FK rejections (BuildContext.java:528-548): zero-FK arm explains why a single-hop key won’t resolve and offers chain-via-intermediate or condition-based alternatives; multi-FK arm pre-fills the first FK name in a {key: ...} example. Re-classification: RecordTableField and RecordLookupTableField free-form-DTO rejections in FieldBuilder.classifyChildFieldOnResultType flip from RejectionKind.DEFERRED to AUTHOR_ERROR since R1’s @batchKeyLifter already closes them; the [deferred] prefix was misleading authors into thinking they’re blocked on a future release. Adjacent fix: SDL source paths in the validator’s gcc-style file:line:col: error: lines relativise against ctx.basedir() via GraphQLRewriteGenerator.relativiseSourceName so the path shrinks to the natural project-relative form (e.g. src/main/resources/schema/.../sak.graphqls); falls back to the original string when the source is null, not absolute, or sits outside basedir. No structural changes to the validator pipeline; existing substring assertions still match (1219 unit-tier tests pass).

  • Multi-table interface / union fetchers (stub-interface-union-fetchers, R36, 033db82 + 201b57b + fcb04c6 + 171f605 + 8f0a0a4 + 33a0670): lifts six *Field variants out of NOT_IMPLEMENTED_REASONS (QueryTableInterfaceField, ChildField.TableInterfaceField, QueryInterfaceField, QueryUnionField, ChildField.InterfaceField, ChildField.UnionField) into native SQL emission across two tracks. Track A (single-table, discriminator-column shape): QueryTableInterfaceField and ChildField.TableInterfaceField emit a single SELECT against the discriminator-bearing parent table with per-participant LEFT JOINs gated on discriminatorColumn and per-occurrence aliases for cross-table @reference participant fields (lifted as ChildField.ParticipantColumnReferenceField); buildDiscriminatorFilter fires the discriminator predicates as a SQL IN (...) clause; JooqCatalog.findColumn resolves SQL column names from logical names; selection-set gating uses graphql-java 25’s Type.field form. Track B (multi-table polymorphic, two-stage shape): new MultiTablePolymorphicEmitter produces a stage-1 narrow UNION ALL projecting (typename, pk0..pkN, sort) across participants, then dispatches stage-2 per typename through ValuesJoinRowBuilder (the same row-builder R55 collapsed for _entities / Query.nodes / @lookupKey) with explicit per-PK-slot t.<col>.eq(input.field(...)) ON predicates. Composite PKs project DSL.jsonbArray(...) as __sort__ so cursor-decode round-trips via ConnectionHelperClassGenerator.encode/decode’s existing JSONB conversion; child fields auto-discover their FK join paths via `FieldBuilder.ctx.parsePath per (parentTable, participantTable) pair. Connection mode: root and child connections share buildStage1ConnectionBlock, which lifts the per-branch UNION ALL into a Table<?> pagesTable so the same derived-table reference backs the page query and the ConnectionResult totalCount. Child-connection emission uses a DataLoader-batched windowed CTE (buildBatchedConnectionFetcher plus buildBatchedConnectionRowsMethod): typed parentInput VALUES widens to Row<N+1> for composite-PK parents (capped at 22 to fit jOOQ’s typed Row22 ceiling), per-branch JOIN parentInput ON <participant>.<fk> = parentInput.<parent_pk> emits the position-aligned composite-FK AND-chain, and a ROW_NUMBER() OVER (PARTITION BY __idx__ ORDER BY page.effectiveOrderBy()) outer filter caps each parent’s rows at page.limit(); per-parent ConnectionResult shares one pagesTable. The page-rows query collapses N parents to 1 SQL statement, asserted by addressOccupantsConnection_dataLoaderBatchesAcrossParents and projectItemsConnection_dataLoaderBatchesAcrossParents. Validator: validateMultiTableParticipants rejects PK-less participants and PK-arity mismatches; validateChildConnectionParentPk rejects empty parent PK and parent-PK arity > 21 as build-time AUTHOR_ERROR instead of codegen-time IllegalStateException. The earlier validateMultiTableConnectionConstraints (the arity-1 reject from B4a) deletes once Item 1 generalises connection mode to composite-PK participants via the JSONB sort key. TypeResolver wiring: GraphitronSchemaClassGenerator iterates non-Node InterfaceType / UnionType alphabetically and reads the synthetic __typename column projected by stage-1. Surface collapse (Item 3): the 5-arg and 6-arg emitConnectionMethods overloads and the per-parent inline branch retire (B4c-1 was promoted same-day to B4c-2’s batched form); the dispatcher in emitConnectionMethods is now a single parentTable != null switch on the 7-arg signature, called directly from TypeFetcherGenerator’s four interface / union arms. Test surface: unit-tier coverage in `TypeFetcherGeneratorTest, GraphitronSchemaClassGeneratorTest, InterfaceFieldValidationTest, and UnionFieldValidationTest (1219 total); execution-tier (233 total) covers cross-table participant fields, multi-table polymorphic root and child, connection pagination plus after-cursor plus totalCount plus inline-fragment dispatch, multi-parent DataLoader batching ratchet, composite-PK participants via paged_a / paged_b fixture, and composite-PK parents via synthetic project (org_id, project_id) with project_note / project_event children. Out of scope: mixed-PK-arity-or-type alignment beyond JSONB-encoded sort (PK column-name collisions across participants stay a follow-up); stage-1-as-CTE optimisation (the straight UNION ALL form is sufficient until profiling says otherwise); mixing with NodeId encoding for relay round-trip (per-field @nodeId projections continue in stage-2’s typed Record path); NodeIdReferenceField JOIN-projection form (R50 follow-up); Node interface TypeResolver (already wired via QueryNodeFetcher.registerTypeResolver). Priority number #3 is embedded in emitted reason strings consumed by existing schema authors and must stay stable. Build green: mvn -f graphitron-rewrite/pom.xml install -Plocal-db SUCCESS on Java 25; 1219 unit plus 233 execution all pass.

  • Argument-level @nodeId architectural tightenings (argument-level-nodeid, R40, 5064a16 + 9192bf7 + 5891293 + 9232887): the argument-level @nodeId machinery was already shipping correct user-visible behaviour out of R50; this item closes three structural seams a design review surfaced. Phase 1: Resolved.FkTarget splits into DirectFk / TranslatedFk sub-arms. The positional-match predicate between FK target columns and NodeType key columns moves from inline checks at two call sites (FieldBuilder.classifyArgument, BuildContext.classifyInputField) into the resolver itself, which picks the variant once. Both call-site projections (argument-side and input-field-side) consume the variant; the inline sameColumnsBySqlName check deletes from FieldBuilder. BuildContext.classifyInputField was previously not running the predicate at all, silently letting the pathological FK-target shape through; the lift closes that asymmetric-gating gap. The shared translatedFkRejectionReason method names the R57 hint substring. Phase 2: LookupValuesJoinEmitter.addRowBuildingCore branches the per-row decode site on CallSiteExtraction.NodeIdDecodeKeys. ThrowOnMismatch keeps the existing GraphqlErrorException for synthesised lookup-key paths where a wrong-type id is a contract violation; SkipMismatchedElement emits continue and tracks an effective row count, returning Arrays.copyOf(rows, effective) when shrunk. LookupMapping.LookupArg.DecodedRecord retypes its decode slot from HelperRef.Decode to CallSiteExtraction.NodeIdDecodeKeys so the failure-mode arm rides on the model. FieldBuilder.classifyArgument’s same-table arg arm flips from `Throw to Skip, restoring the originally-specified Skip semantics over the first pass’s expedient Throw; the implicit scalar-ID arm (no @nodeId, NodeId-backed table) keeps Throw for the synthesised lookup-key path. Phase 3: NodeIdArgPlan pre-resolves every @nodeId-decorated leaf reachable from a table-bound field’s argument set in one walk, threaded through resolveTableFieldComponents → classifyArguments → classifyArgument; the three previous walks (findSameTableNodeIdUnderAsConnection, walkInputTypeForSameTableNodeId, hasSameTableNodeIdAnywhere) collapse into reads of the plan. The @asConnection rejection, the lookup-promotion gate, and per-arg classification now share one classification pass instead of re-resolving each leaf three times. Load-bearing key: nodeid-fk.direct-fk-keys-match annotates NodeIdLeafResolver.resolve (producer) and three consumers (FieldBuilder.projectFilters, FieldBuilder.walkInputFieldConditions, BuildContext.classifyInputField); LoadBearingGuaranteeAuditTest picks up the pairing automatically. Test surface: pipeline-tier InputFieldFkTargetNodeIdCase.FK_TARGET_PATHOLOGICAL_KEY_MISMATCH_DEFERRED_INPUT covers the input-field-side asymmetric-gating closure; ArgumentSameTableNodeIdCase extraction assertions flipped from ThrowOnMismatch to SkipMismatchedElement. Resolver-tier NodeIdLeafResolverTest is the first resolver-tier unit test for an R6 resolver (DirectFk on matching keys, TranslatedFk on the parent_node + child_ref reproducer where the FK targets parent_node.alt_key but the NodeType key is parent_node.pk_id, DirectFk again on the input-field side); a new GraphitronSchemaBuilder.buildContextForTests seam exposes the wired BuildContext after type classification but before field classification. Execution-tier GraphQLQueryTest triplet (filmsByNodeIdArg_malformedIdMixedWithWellFormed_returnsWellFormedSubset, _allMalformedIds_returnsNoRows, _emptyList_returnsNoRows) covers the partial-decode skip path, the all-skipped short-circuit, and the empty-input edge. Out of scope: R57 (TranslatedFk JOIN-with-translation emission), multi-hop FK-target on the input side, mutation-key @nodeId args, and Record1 raw-cast template factoring. Full mvn install -Plocal-db clean.

  • @batchKeyLifter directive re-enables DataLoader batching on @record parents that lack catalog FK metadata (batchkey-lifter-directive, R1, 07e6954 + d12e60d + 4283fb2 + 7c284a1 + b5c6749 + a1a5e29): closes the two RecordTableField / RecordLookupTableField "requires a FK join path and a typed backing class for batch key extraction" deferred rejections in FieldBuilder.classifyChildFieldOnResultType for free-form DTO parents (PojoResultType and JavaRecordType with non-null fqClassName). The schema author supplies a static Java method that lifts a RowN<...> batch key out of the parent DTO; the classifier reflects on it once at build time, validates the per-position column-class match against the directive’s targetColumns, and produces a BatchKey.LifterRowKeyed carrying a JoinStep.LiftedHop (target table + key columns, single-hop by construction) plus a LifterRef(ClassName, String) typed reference (sibling of MethodRef, shaped after R50’s HelperRef precedent). The emitter feeds the result into the existing column-keyed DataLoader path with no identity branching: target accessors come from a new JoinStep.WithTarget capability mixed in by FkJoin and LiftedHop; key extraction comes from the lifter call. Surface: new directive on FIELD_DEFINITION (@batchKeyLifter(lifter: ExternalCodeReference!, targetColumns: [String!]!)); new BatchKey.LifterRowKeyed permit (sealed hierarchy now five variants: RowKeyed, RecordKeyed, MappedRowKeyed, MappedRecordKeyed, LifterRowKeyed); new BatchKey.ParentKeyed and BatchKey.RecordParentBatchKey sealed sub-interfaces splitting the variant axis (the four catalog records expose parentKeyColumns() renamed from keyColumns(); LifterRowKeyed exposes targetKeyColumns() via the contained LiftedHop); the interface-level BatchKey.keyColumns() accessor removed (a shared accessor with variant-dependent meaning violated Sealed hierarchies over enums); new JoinStep.LiftedHop permit (sealed hierarchy now three variants: FkJoin, ConditionJoin, LiftedHop); new JoinStep.WithTarget capability mixed in by FkJoin and LiftedHop; new BatchKeyLifterDirectiveResolver standalone resolver, sibling to R6’s ten directive/projection resolvers, so classifier-side directive logic stays out of FieldBuilder. Renames and narrowings: GeneratorUtils.buildRecordKeyExtraction → buildRecordParentKeyExtraction, parameter narrowed from BatchKey to BatchKey.RecordParentBatchKey; GeneratorUtils.buildKeyExtraction parameter narrowed from BatchKey to BatchKey.ParentKeyed; both narrowings turn mis-routing of the @service-only permits into compile errors rather than runtime throws. TypeFetcherGenerator.buildRecordBasedDataFetcher no longer casts to BatchKey.RowKeyed. SplitRowsMethodEmitter.emitParentInputAndFkChain reads target accessors uniformly via the JoinStep.WithTarget capability; sealed-switch usage is reserved for the JOIN-on predicate (the genuine identity fork). The (JoinStep.FkJoin) and (BatchKey.RowKeyed) casts removed. Validator gate (Invariant #10, R1 Phase 2e): RecordTableField and RecordLookupTableField reject single-cardinality returns at validate time, promoting the previous SplitRowsMethodEmitter.unsupportedReason runtime stub to a build-time AUTHOR_ERROR. The stub is replaced by an IllegalStateException (post-validate reachability is a classifier bug). Emitter fix (Phase 2f): SplitRowsMethodEmitter.buildListMethod WHERE-filter loop unconditionally cast every JoinStep to FkJoin, throwing ClassCastException for LiftedHop paths. Fixed with if (!(path.get(i) instanceof JoinStep.FkJoin hop)) continue; ; LiftedHop carries no FK-side filter to apply, so the loop skips it. Load-bearing keys: per-fact @LoadBearingClassifierCheck / @DependsOnClassifierCheck pairs lifter-classifies-as-record-table-field and lifter-batchkey-is-lifterrowkeyed (both with producer pairs on BatchKeyLifterDirectiveResolver.resolve). The single-hop invariant is a structural model property (LifterRowKeyed holds one LiftedHop, not a list) documented in a plain javadoc comment on the rows-method prelude rather than as a keyed fact. LoadBearingGuaranteeAuditTest is unchanged. Test surface: BatchKeyLifterCase pipeline-tier coverage of the classifier matrix (POJO_PARENT_VALID_ROW1_LIST etc.) plus scalar-return rejection; MutationPayloadLifterTest execution-tier coverage with three CreateFilmPayload rows (languageId [1, 2, 1]) asserting one DataLoader dispatch with two distinct keys (DataLoader key-deduplication: 3 input rows → 2 batched VALUES tuples), the SQL contains language_id and "language" and (values (0, ?), (1, ?)), and per-parent Language lists resolve correctly (English × 2, Italian × 1). Hand-rolled fixture service (no DSLContext parameter) ensures the QUERY_COUNT == 1 assertion is clean ; the only JDBC round-trip is the lifter-batched language lookup. Documentation: rejection messages in FieldBuilder and ServiceCatalog.dtoSourcesRejectionReason now reference the live directive instead of the roadmap-file path; code-generation-triggers.adoc directive table gains a @batchKeyLifter row on the @record-parent child-fields table and the Source Map’s BatchKey / JoinStep permit listings updated for the new variants; rewrite-design-principles.adoc gains a "DTO-parent batching" subsection cross-linked from "Column value binding". Full mvn install -Plocal-db clean.

  • Load-bearing classifier guarantee audit annotations (load-bearing-guarantee-audit, R21, 9acdf3f): codifies the "classifier rejection becomes emitter assumption" pattern named in rewrite-design-principles.adoc § "Classifier guarantees shape emitter assumptions" as a runtime-discoverable annotation pair under no.sikt.graphitron.rewrite.model. LoadBearingClassifierCheck(key, description) marks the producer arm; DependsOnClassifierCheck(key, reliesOn) (repeatable via DependsOnClassifierChecks) marks each emitter site that relies on it. LoadBearingGuaranteeAuditTest walks target/classes under the rewrite package root, groups by key, and fails on (a) any consumer key without a matching producer, (b) any duplicate producer key, (c) blank description / reliesOn. A non-empty class-walk assertion guards against vacuous passes when the test is run before compile or from the wrong cwd. Producers without consumers are allowed (some checks reject for hygiene rather than because an emitter relies on them). The audit logic is exposed via package-private audit(Iterable<Class<?>>) returning List<AuditViolation> so a meta-test can exercise the failure-detection against a deliberate-violation fixture (auditfixture/OrphanedConsumer) without disturbing the production scan, keeping the audit’s own failure-detection durable across walker refactors. Sites annotated on landing: producers service-catalog-strict-tablemethod-return (ServiceCatalog.reflectTableMethod) and service-catalog-strict-service-return (ServiceCatalog.reflectServiceMethod) paired with TypeFetcherGenerator.buildQueryTableMethodFetcher / buildQueryServiceTableFetcher consumers; column-field-requires-table-backed-parent (FieldBuilder.classifyChildFieldOnTableType) paired with TypeFetcherGenerator.generateTypeSpec’s `case ChildField.ColumnField arm; error-channel.mappings-constant (FieldBuilder.resolveErrorChannel paired with ErrorMappingsClassGenerator.generate); plus the consumerless error-type.path-message-fields producer on TypeBuilder.buildErrorType. Inverse asymmetry (a new emitter that should depend on a guarantee but forgets @DependsOnClassifierCheck) is acknowledged out of scope: that drift mode falls back to the generated *Fetchers compile failure that the principles doc already names as the safety net. Documentation: rewrite-design-principles.adoc § "Classifier guarantees shape emitter assumptions" gains an enforcement paragraph naming the annotation pair and pointing forward at adding annotations on every new load-bearing classifier check. The annotation triple, audit test, fixture, and design-doc paragraph all landed as part of 9acdf3f (R12 §2c) because R12’s error-channel.mappings-constant was the first new live producer and the infrastructure was needed to gate it.

  • EntityFetcherDispatch per-typeId VALUES emission collapsed onto a shared row-builder (entityfetcherdispatch-lookup-pipeline-collapse, R55, 5aec7cd + 8ac503c + aee21f6): the typed Row<N+1> array, the arity-22 cap, the per-cell DSL.val(value, table.COL.getDataType()) construction, the DSL.values(rows).as(alias, "idx", "<sqlName>", …) aliasing, and the USING-args list now live in one place. New ValuesJoinRowBuilder helper (graphitron/src/main/java/no/sikt/graphitron/rewrite/generators/util/ValuesJoinRowBuilder.java) is consumed by both LookupValuesJoinEmitter (@lookupKey root and inline-child paths) and SelectMethodBody (federated _entities plus Query.node / Query.nodes dispatch); the f-E SQL-shape pin (GraphQLQueryTest.nodes_perTypeIdBatch_emitsValuesJoinOrderByIdxShape) gates that the values / join / order by substring shape survives across both call sites. Helper API generic over caller slot: methods take List<S> plus Function<S, ColumnRef> projection plus a directive-context String (used in arity-cap and empty-slots error messages). The lookup site keeps its rich Slot record (argName, RootSource, decode bindings) and passes Slot::targetColumn; the dispatcher passes Function.identity() against List<ColumnRef>. The cellsCode value-expression callback receives the caller’s slot back, so no parallel-list bridge is required. Caller-local pieces (kept off the helper): the for-loop body that fills rows[i] (lookup site does composite-key extraction and the per-row DecodedRecord NodeId decode + GraphqlErrorException on null; dispatcher reads binding[0] / binding[1]); the idx cell expression (lookup uses DSL.inline(i), dispatcher uses DSL.val(idx, Integer.class), both render to a typed Field<Integer>); any extra projections beyond the join (the dispatcher’s DSL.inline("<TypeName>").as("__typename") and the materialised idxCol); the join syntax; and the .where(condition) / .orderBy(idxCol) chain. Reviewer-pass deltas (commit 8ac503c): the original change flipped the dispatcher to .using(...) for symmetry with the lookup root path; the reviewer reverted it to .on(t.COL.eq(input.field("col", T.class)).and(…)) because the dispatcher’s SELECT projection comes from <TypeName>.$fields(env.getSelectionSet(), t, env) which references t.<col> directly, and USING collapses joined columns at render time, risking interactions with $fields-emitted projections that include the joined key columns themselves. The helper still exposes usingArgs for the lookup root path; the dispatcher’s join syntax is documented on SelectMethodBody’s class Javadoc. The `Condition condition = DSL.noCondition(); declaration sits before the join body in both call sites so the SELECT chain stays symmetric and gives R36 Track B (per-typename interface filters) and any future per-arm filter a hook to AND into; jOOQ folds noCondition() away at render time. Other reviewer-pass deltas: dropped a transitional ValuesJoinRowBuilder.Slot(ColumnRef) record that required parallel-list bookkeeping at the lookup site; added an empty-slots guard to rowTypeArgs (defensive ; both upstream classifiers already enforce non-empty key columns); arity-cap and empty-slots messages now embed a directive context ("@lookupKey" / "@key"), restoring the schema-author UX of the pre-collapse error messages; MAX_ARITY is package-private (tests are in-package, no external caller). Test surface: 16 ValuesJoinRowBuilderTest cases pin arity (1, 5, 21), the 22-cap with directive context, the empty-slots guard with directive context, alias args, USING args, both idx-cell shapes (lookup DSL.inline(i), dispatcher DSL.val(idx, Integer.class)), the rich-slot callback contract, and the Row<N+1>[] / Table<Record<N+1>> convenience helpers. The f-E regression test continues to pass; FederationEntitiesDispatchTest (16 cases) all green; GraphQLQueryTest (141 cases) all green. Line deltas: LookupValuesJoinEmitter 505 → 458, SelectMethodBody 164 → 159; new helper 185 lines + 196 lines of unit tests. Net diff −68 lines of generator code; net code-plus-tests 313. *Downstream consumer*: R36 Track B stage 2 (native multi-table polymorphism, shipped at `+ffa59e4) is the third caller of `ValuesJoinRowBuilder, using the dispatcher-shape .on(...) callsite per the same <TypeName>.$fields(...) constraint. Follow-up nits (commit aee21f6): dispatcher’s ColumnRef projection switched from c -> c to Function.identity() (singleton, saves one lambda allocation per emit); unused Slot lambda parameters renamed to _ in three places (the lookup site’s lambda still reads its slot param, so explicit naming stays there); spec design-section table for join syntax now ends with an inline pointer to "Reviewer pass deltas above" so a reader landing mid-doc isn’t misled by the original "Switch dispatcher to .using(…​)" decision.

  • FieldBuilder decomposed onto the cross-cutting-concern axis (decompose-fieldbuilder, R6, Phase 1 at 3f9b84c; Phases 2a/2b/2c at 201c2f0 + c819027 + 9e8fc46; Phase 5 at fd94f37; Phases 6a/6b/6c/6d/6e at 38b143c + 67c543d + 9766982 + 679c560 + 56cd3a0; Phase 7 at 42f8259; Phase 8 final mop-up at 84f4be7; review-driven shape tightening at cea16e0): the parent-context-first dispatch (classifyQueryField / classifyMutationField / classifyChildField*) survives as a thin orchestrator that calls a fixed pipeline of resolvers and projects each Resolved arm into the correct GraphitronField variant. Each cross-cutting concern lifted into its own resolver returning a sealed result, sibling to ArgumentRef’s sealed-variant pattern (the canonical example of Principle 7’s "builder-internal sealed hierarchies for multi-target classification" in `rewrite-design-principles.adoc). Directive resolvers (eliminate inline duplication and byte-identical rejection prose across classify arms): ServiceDirectiveResolver (@service, four-arm classify lift, sealed Resolved with Success.{TableBound, Result, Scalar} / ErrorsLifted / Rejected), TableMethodDirectiveResolver (@tableMethod, two-arm lift, Resolved.{TableBound, NonTableBound, Rejected} gated by isRoot), ExternalFieldDirectiveResolver (@externalField, single-arm lift, Resolved.{Success, Rejected}), LookupKeyDirectiveResolver (@lookupKey, three-arm lift, Resolved.{Ok, Rejected} with resolveAtRoot / resolveAtChild(_, withSplitQuery) entry points). Projection resolvers (lift bundled monoliths into focused units): OrderByResolver (~230 lines, sealed Resolved.{Ok, Rejected} owning the canonical @defaultOrder fallback message), LookupMappingResolver (pure projectForLookup ref-walker, total projection so no Resolved wrapper), PaginationResolver (clusters projectPaginationSpec + isPaginationArg + resolveDefaultFirstValue since they’re all pagination semantics), ConditionResolver (@condition resolution, two sealed result types ArgConditionResult / FieldConditionResult each {None, Ok, Rejected} replacing the prior dual-signal pattern), InputFieldResolver (plain-input-field classification wrapping BuildContext.classifyInputField), MutationInputResolver (DML @mutation input classification + return-type validation + @mutation(typeName:) parsing, Resolved.{Ok, Rejected}), EnumMappingResolver (the enum-mapping axis: buildTextEnumMapping / validateEnumFilter / deriveExtraction / enrichArgExtractions / buildLookupBindings, lifted last because the helpers fan out across argument classification rather than clustering with any single earlier phase). Final mop-up (Phase 8): conflict detection (detectQueryFieldConflict, detectChildFieldConflict) stays as private helpers on FieldBuilder since the methods are trivial single-call-site logic with no isolated test surface to gain; the remaining fb-coupled callbacks (buildWrapper, parseExternalRef, parseContextArguments, liftToErrorsField, fieldArgumentNames) likewise stay on FieldBuilder since migrating them to BuildContext would muddy that class’s schema/jOOQ-classification concern; eleven orphan imports (one model + ten BuildContext static imports left behind by the directive- and projection-axis lifts) removed, and fieldArgumentNames’s redundantly fully-qualified `Collectors / LinkedHashSet references collapsed onto the already-present imports. Review-driven shape tightening (cea16e0): five contained changes addressing dual-signal patterns and unused parameters surfaced by reviewing the lifts: LookupKeyDirectiveResolver drops unused (BuildContext, ServiceCatalog, FieldBuilder) constructor params; OrderByResolver consumes the classified ArgumentRef.OrderByArg directly instead of looking the GraphQLArgument up by name and re-walking the input type (drops 4 now-unreachable rejection arms + 3 orphan imports); EnumMappingResolver.validateEnumFilter’s null/fqcn/empty-string tri-state replaced with a sealed `EnumValidation.{NotEnum, Valid(fqcn), Mismatch(message)}; FieldBuilder.TableFieldComponents’s six-nullable-field record replaced with a sealed `{Ok, Rejected} interface (six consumer + five producer sites updated); MutationInputResolver lifts the @mutation(typeName:) raw String into a DmlKind enum + sealed DmlKindResult.{Absent, Kind, Unknown} (replaces 8+ string-equality comparisons across three sites with exhaustive enum switches and removes an "unreachable: typeName=…​" default arm). Net result: FieldBuilder shrinks from 3,301 lines to ~2,534 lines (-1,007 against the counterfactual no-R6 trajectory; live size also reflects R12’s concurrent growth) and ten resolver siblings live as standalone files (~240 / 148 / 117 / 90 / 289 / 88 / 109 / 176 / 73 / 317 / 276 lines respectively), each independently testable and aligned with the sealed-result pattern that ArgumentRef set as the precedent.

  • NodeId lifted out of the model (lift-nodeid-out-of-model, R50, phases d/e1/e2-foundation/e2-rest/e3/e4a/e4b/e4c at 7bf0303 + 2635d97 + b36f230 + 9e38cc9 + 1b07b72 + f2ba2c5 + 995bb29 + a923694; phases f-A/f-B/f-C/f-D/f-E at 67999cf + d43f1e3 + b21a152 + 8fe072f + 3298ac7; phases g-A/g-B/g-C at 29734fb + 6c54435 + e4ac4ee; encoder cleanup at 1275396 + 8a12231; status flip at 0b26872; post-review notes at 3459225; final retirements at fad83a7; R55 follow-on filed at 72ae5cc): wire-format encoding and decoding for @nodeId ids now lives at the DataFetcher boundary; the classifier model and emitted query builders below it see decoded key tuples and standard column predicates rather than NodeIdEncoder.hasIds(...) calls reaching across the boundary. Wire-shape variants retired: InputField.NodeIdField / NodeIdReferenceField / NodeIdInFilterField / IdReferenceField, ChildField.NodeIdField / NodeIdReferenceField, BodyParam.NodeIdIn, LookupMapping.NodeIdMapping, ArgumentRef.ScalarArg.NodeIdArg. New boundary taxonomies: CallSiteExtraction.NodeIdDecodeKeys sealed into two arms (SkipMismatchedElement for filter call-sites where a malformed id short-circuits to "no row matches"; ThrowOnMismatch for lookup-key / mutation-key call-sites where a wrong-type id is a contract violation); CallSiteCompaction sealed root with Direct (plain projection) and NodeIdEncodeKeys(HelperRef.Encode) (encode-on-projection) arms; the third failure mode (NullOnMismatch for Query.node / Query.nodes / federated _entities) is dispatcher-driven rather than carrier-driven and lives in EntityFetcherDispatchClassGenerator. Composite-key column carriers: new InputField.CompositeColumnField / CompositeColumnReferenceField, ChildField.CompositeColumnField / CompositeColumnReferenceField, ArgumentRef.ScalarArg.CompositeColumnArg for arity > 1 NodeIds; arity-1 cases stay on the existing single-column carriers (which gain extraction / compaction slots). The Composite* variants narrow their boundary slot to the only arm the classifier produces (NodeIdDecodeKeys on input, NodeIdEncodeKeys on output) at the type system level rather than asserting via validator rule. BodyParam.ColumnPredicate sealed sub-taxonomy replaces the old ColumnEq(boolean list) shape with four predicate-arm records: Eq / In (single column) and RowEq / RowIn (composite-key tuples emitting DSL.row(c1, ..., cN).eq(...) / .in(...)). Lookup arg restructure: LookupMapping.ColumnMapping retypes from a flat List<LookupColumn> to List<LookupArg> sealed into ScalarLookupArg (single-key target with optional NodeId decode) / MapInput (composite-key Map input from R5’s @lookupKey) / DecodedRecord (composite-PK NodeId where the decode runs once per row at the arg layer); InputColumnBinding generalises from a flat record into a sealed split (MapBinding / RecordBinding), narrowly typed per arm so the source-shape homogeneity is type-enforced rather than validator-asserted. HelperRef new sealed sibling of MethodRef with separate Encode / Decode arms because the same List<ColumnRef> plays semantically different roles on each side (call-site Java parameter list vs returned RecordN<...> shape); GraphitronType.NodeType gains pre-resolved encodeMethod / decodeMethod fields read by every emitter and the encoder generator from one source of truth. NodeIdEncoder API: per-Node-type encode<TypeName>(...) / decode<TypeName>(String) → RecordN<T1..TN> helpers replace the generic encode("typeId", ...) / decode("typeId", ...) surface; peekTypeId(String) stays as the only generic public method (used by typeId-fanout sites); the generic encode / decodeValues bodies become private. Deleted: hasIds / hasId (query-builder helpers that did not belong in the encoder), coerceValue (the per-type decoders inline getDataType().convert(...) per slot statically), canonicalize (no callers; Base64.getUrlDecoder accepts both padded and unpadded forms). Single-hop emission, two shapes: rooted-at-child (no JOIN, FK source columns are the keys) ships fully; rooted-at-parent (single-hop JOIN where FK source columns differ from target’s keyColumns) ships classifier-only with FetcherEmitter runtime stubs, deferred to R24’s expanded scope. Multi-hop FK and condition-join correlated-subquery emission stays in R24. MutationField.DmlTableField.nodeIdMeta retypes to encodeReturn: Optional<HelperRef.Encode> so the DML emitter no longer reconstructs the helper reference from a typeId string at emission time; JooqCatalog.NodeIdMetadata survives only as a classifier-time intermediate. Validator coverage lands one arm per new sealed variant (two NodeIdDecodeKeys, two CallSiteCompaction, four ColumnPredicate, three LookupArg, two InputColumnBinding, five Composite* carriers); TypeFetcherGenerator.NOT_DISPATCHED_LEAVES shrinks. Load-bearing classifier guarantees annotated at three keys (nodeid.decode.failure-mode, columnpredicate.column-arity, compaction.encode-keys) so emitter assumptions are tracked back to classifier sites. Fixture growth: nodeidfixture gains a composite-PK Bar Node type for [ID!] @nodeId(typeName: "Bar") row-IN coverage and a rooted-at-parent parent_node + child_ref shape (FK targets non-PK unique column) ready to drive R24’s emitter coverage. Test surface: every @nodeId execution test continues to round-trip (Query.node, Query.nodes, federated _entities, same-table filter, rooted-at-child reference, composite-PK lookup); SQL inspection via ExecuteListener confirms emitted bodies are c.eq / c.in / DSL.row(...).eq / .in over decoded key tuples rather than encoded String ids; Query.nodes per-typeId batch SQL pinned to VALUES + JOIN + ORDER BY idx shape (regression test catches dispatcher fallback to legacy WHERE row-IN); failure-mode parity verified per arm. Post-review cleanup retired two compat fallbacks (InputField.NodeIdField and NodeIdReferenceField’s "classified-but-inert" arms in `BuildContext) that survived the wire-shape variant deletions; both reroute to InputFieldResolution.Unresolved with pointed AUTHOR_ERROR reasons. Coupling: R20 (IdReferenceField code generation) tombstoned and deleted in this transition (its execution-tier coverage is in R50’s pipeline test surface); R24 expanded to absorb rooted-at-parent single-hop JOIN-with-projection emission alongside its original multi-hop / condition-join scope; R55 filed as a Backlog item to collapse EntityFetcherDispatch’s bespoke per-typeId VALUES emission onto `LookupValuesJoinEmitter (the SQL shape was pinned by phase f-E but the two pipelines remain parallel). R40 (argument-level @nodeId) reduced to a small classifier-only follow-on. Inbound roadmap references in retire-synthesis-shims.md (R27) and faceted-search.md (R13) updated to name the post-R50 column-shaped successors. Stale {@link InputField.IdReferenceField#targetTypeName} javadoc in BuildContext and "R50 phase b2b" stub messages in FetcherEmitter / TypeFetcherGenerator retexted to point at R24.

  • Consolidated test-tier guide shipped (rewrite-test-tier-guide, R29, f621097 + bb83da6): four JUnit 5 meta-annotations (@UnitTier, @PipelineTier, @CompilationTier, @ExecutionTier) added to graphitron-fixtures main scope; applied to every @Test-bearing class in graphitron (87 classes) and graphitron-test (8 classes); GeneratorDeterminismTest carries @Tag("cross-cutting") directly as the sole cross-cutting test. Enforcement test added per-module: each walks its own target/test-classes tree and fails the build if any @Test-bearing class lacks exactly one tier identity. New graphitron-rewrite/docs/testing.adoc with decision rubric, per-tier sections, module-location vs. tier table, and build commands. Cross-links: rewrite-design-principles.adoc tier sections trimmed to one-liner pointers; docs/README.adoc Detailed reference list gains the new file; .claude/web-environment.md gains a one-line pointer. Javadoc sweep replaced "Level N" and mismatched tier prose with tier annotations across FieldValidationTestHelper, GraphitronSchemaBuilderTest, GeneratedSourcesSmokeTest, IdempotentWriterTest, and GeneratorDeterminismTest. Unblocks R25 rebalance-test-pyramid which depends on the canonical tier names. Review fixes (bb83da6): two broken xref: links to .claude/web-environment.md replaced with inline-code references (file is not staged in the AsciiDoc tree); ten -- em-dash occurrences replaced with semicolons or colons per the CLAUDE.md writing convention.

  • graphitron-rewrite/docs/README.adoc lifted into an Architecture entry point (rewrite-docs-entrypoint, R28, 1b59f2e + ceb5cde): adds an eight-row module table, a six-stage end-to-end pipeline tour (RewriteSchemaLoader → GraphitronSchemaBuilder → GraphitronSchemaValidator → Generators → JavaFile.writeToPath → consumer compile) naming the directives.graphqls injection-before-classification and orphan-sweep-after-every-emit ordering invariants that code-generation-triggers.adoc’s zoomed-in classification diagram leaves implicit, and a closing "Detailed reference" index. `workflow.adoc’s "Canonical path" gains a one-sentence pointer at `computed-field-with-reference in the changelog as a recent end-to-end exemplar. Per-module READMEs deliberately not maintained; the inline table is the orientation surface. Phase 1 (drop the inherited #4 numbering, add a real preamble) was absorbed by R9’s AsciiDoc migration.

  • ExternalCodeReference.argMapping for Java-param binding (external-code-reference-arg-mapping, R53, 4a6b731 + d120892): introduces a single canonical channel for naming the GraphQL→Java parameter binding on every method-backed call: @service, @tableMethod, and every @condition site (field-level, argument-level, input-field-level, path-step). The schema gains an argMapping: String field on ExternalCodeReference carrying a comma-separated javaParam: graphqlArg mini-DSL (target-on-left, matching the internal Map<javaTarget, graphqlSource> shape and the @experimental_constructType.selection convention). Unmentioned parameters bind by identity; whitespace and text-block input are tolerated. The R41 per-arg @field(name:) Java-binding semantic is retired in the same change without a deprecation cycle (R41 was unshipped to consumers); @field(name:) reverts to its column-binding axis on table-backed sites and its db-string mapping on ENUM_VALUE. ArgBindingMap collapses the previous forField / identityFor* family to one axis-agnostic factory of(Set<String>, Map<String, String>) returning sealed Result.{Ok, UnknownArgRef}; a new parser parseArgMapping(String) returns sealed ParsedArgMapping.{Ok, ParseError} and enforces unique Java targets, with order-preserving iteration via LinkedHashMap + Collections.unmodifiableMap. FieldBuilder.ExternalRef and BuildContext.ConditionDirective retype to carry argMapping and argMappingError separately from lookupError; failure precedence makes lookupError win over argMappingError so "I can’t resolve the class" reads ahead of "and your argMapping has a typo." Wire-through covers all seven reflect call sites: resolveServiceField, the two @tableMethod arms (root + child), buildArgCondition, buildFieldCondition, BuildContext.resolveConditionRef, and buildInputFieldCondition. Path-step @condition resolves with an empty slot set, so any non-empty argMapping rejects through UnknownArgRef; resolveConditionRef returns a new ConditionResolution(ref, error) record so the path-step caller surfaces the parser/typo with site context ("path-step @condition: …") rather than the previous generic "could not be resolved" message. Structural-inertness rejections route through the classifier rather than the schema validator (deviation from the plan, captured as an implementation note): parseExternalRef rejects argMapping on @externalField; TypeBuilder.buildResultType and buildNonTableInputType reject on @record; the enum-classify branch rejects on @enum. Tests across all upper tiers: new ArgBindingMapTest (15 cases covering parser empty/blank, duplicate Java target, malformed entry, missing colon, text-block input, factory identity baseline, override-claims-slot, two-overrides-binding-to-same-slot, unknown GraphQL arg, path-step empty + non-empty); GraphitronSchemaBuilderTest cases for happy-path, parser-rejected duplicate, pre-reflection unknown arg, post-reflection typo guard, structural-inertness rejections on @externalField/@record/@enum, plus four cross-axis cases (ARG_CONDITION_ARGMAPPING_DUAL_BOUND, FIELD_CONDITION_ARGMAPPING, TABLE_INPUT_FIELD_CONDITION_ARGMAPPING, CONDITION_PATH_ARGMAPPING_REJECTED) that R41’s per-arg design could not express; ServiceCatalogTest post-reflection error messages reframed from @field(name: "X") to argMapping entry 'X: Y'. Pipeline + execute coverage: filmsByServiceRenamed fixture (graphitron-test/src/main/resources/graphql/schema.graphqls) authored directly with argMapping: "filmIds: ids"; GraphQLQueryTest.queryServiceTable_filmsByServiceRenamed_overrideBindsArgToDifferentlyNamedJavaParam round-trips against PostgreSQL. R41 was tombstoned under the workflow’s Discarded terminal-state rule; R53 inherited R41’s reviewed design conclusions but ran its own Spec → Ready review cycle.

  • @service rows-method body ; first iteration (service-rows-method-body + R49 Phase B, befc156): replaces the previously-stubbed body emitted by buildServiceRowsMethod with a working call site. The shared emitter now handles both ServiceTableField and ServiceRecordField, walking MethodRef.params() to build the developer’s call: ParamSource.Sources → the loader’s keys parameter (passed through directly; element-shape conversion is a follow-up), ParamSource.DslContext → a dsl local declared from graphitronContext(env).getDslContext(env) when needed, ParamSource.Arg and ParamSource.Context via the existing buildArgExtraction and getContextArgument paths. ArgCallEmitter.buildMethodBackedCallArgs gains a 4-arg overload accepting a sourcesExpression CodeBlock; the legacy 3-arg overload delegates with null so root-level @service (where Sources is rejected at classifier time per Invariants §2) still throws when it sees the variant. buildServiceRowsMethod takes (BatchKeyField, MethodRef, ReturnTypeRef, perKeyType, parentTypeName, outputPackage); the dispatch site for both service variants threads the variant’s MethodRef through. FilmService.titleUppercase fixture switches from Set<Row1<Integer>> to Set<Record1<Integer>> (classifies as BatchKey.MappedRecordKeyed, framework emits Record1 keys via GeneratorUtils’s `RecordKeyed branch which uses Record.into(Tables.FILM.FILM_ID) for extraction); body fetches each film’s title from the film table and returns Map<Record1<Integer>, String> with uppercased values. Mental-model clarification: Row1<T> is jOOQ’s SQL-expression type for tuple-IN comparisons against the database, not an application-side artifact (no value accessor); Record1<T> extends Row1<T> and adds value1() for application reading. The framework’s continued emission of Row1 keys for the RowKeyed / MappedRowKeyed BatchKey variants is documented as a follow-up ; Set<TableRecord> / List<TableRecord> developer signatures still classify as those variants, so a dev choosing those shapes hits the same wall a Set<Row1<Integer>> dev would. Execution test GraphQLQueryTest.films_titleUppercase_resolvesViaServiceRecordFieldDataLoader exercises the full path: parent SELECT followed by one batched DataLoader round-trip resolving all five films' uppercased titles. R32’s spec body collapsed to a "shipped" pointer with the open follow-ups (element-shape conversion, the Row1 follow-up, strict-return-type validation against field.elementType(), the typed-context-value registry coordination) called out for tracking.

  • ServiceRecordField Phase A ; DataLoader plumbing for child @service with scalar / @record-backed return (service-record-field, b9a6900 + 87a827d + f9bf585 + 85974ac + Phase A close-out): lifts ChildField.ServiceRecordField out of TypeFetcherGenerator.NOT_IMPLEMENTED_REASONS into IMPLEMENTED_LEAVES. Phase B (R32) fills the rows-method body; Phase A ships the variant’s classification, BatchKey carrier, DataLoader registration, lambda + key-extraction emission, and a stub rows-method that throws UnsupportedOperationException at request time. Model: ChildField.ServiceRecordField gains a non-null BatchKey batchKey field and implements MethodBackedField, BatchKeyField; rowsMethodName() follows the same load<X> convention as ServiceTableField so the existing dispatch + key-extraction infrastructure picks the variant up via BatchKeyField pattern matching. elementType() accessor closes the deferral noted in set-parent-keys-on-service.md by deriving the per-key V from the schema directly: ResultReturnType with non-null fqClassName → backing class; ScalarReturnType → standard GraphQL scalar’s Java type (String / Boolean / Integer / Double / String for ID; custom scalars and enums fall back to String until the Phase B consumer scalar registry surfaces typed Java classes); other cases fall through to the reflected outer return on MethodRef.returnType(). Builder: Site 1 (classifyChildFieldOnResultType, @record-typed parent) scalar/record-return arms become RejectionKind.DEFERRED with a roadmap pointer (deriving the batch key would need lifting through the parent chain to a rooted @table, a separate design problem parallel to interface-union dispatch); Site 2 (classifyChildFieldOnTableType, @table-typed parent) lifts the BatchKey via the existing extractBatchKey(MethodRef) helper and constructs the variant. Validator: validateServiceRecordField rejects non-empty joinPath with RejectionKind.DEFERRED until the lift form ships. Generator: buildServiceDataFetcher and buildServiceRowsMethod are parameterised by (ReturnTypeRef, perKeyType) rather than the previous TableBoundReturnTypehard-coded `+RECORD; `ServiceTableField passes RECORD, ServiceRecordField passes field.elementType(). Drive-by fixes uncovered by the first child-@service schema fixture in graphitron-test: dfe.getSelectionSet().getField(<name>) (non-existent API on DataFetchingFieldSelectionSet) → drop the sel extraction and the sel parameter from the rows-method signature (the Phase A stub throws and Phase B will reintroduce whatever shape its body needs); the loader-value-type fix that’s part of elementType() above (the previous RECORD hard-coding meant the generator was correct for ServiceTableField only ; ServiceRecordField would have shipped DataLoader<K, Map<K, V>> had the per-key-type lookup not been added in this Phase). Coverage: six TypeFetcherGeneratorTest cases assert the parameterisation along the new axis (positive scalar single + list, record-backed single, mapped factory selection, mapped rows-method return shape, positional rows-method return shape); two existing serviceField_* rows-method-signature tests are updated for the dropped sel parameter. Builder-tier coverage: GraphitronSchemaBuilderTest.NonTableParentCase.SERVICE_FIELD_ON_RESULT_TYPE flips from "ServiceRecordField" to "DEFERRED with @record-parent reason"; ServiceFieldValidationTest.RecordCase.NO_PATH expects no errors (variant is implemented) and WITH_LIFT_CONDITION expects the new joinPath DEFERRED rejection. New fixture: FilmService.titleUppercase(Set<Row1<Integer>>, DSLContext) -> Map<Row1<Integer>, String> (Phase A signature only; body throws to mirror the generated rows-method’s stub) plus Film.titleUppercase: String @service(...) on graphitron-test/schema.graphqls. The fixture compiles and is reachable from the schema; Phase B (R32) replaces the body and adds an end-to-end execution-tier test against PostgreSQL. Strict-return-type validation against field.elementType() is also Phase B’s deliverable since the structural unwrapping (Map<KeyType, V> vs List<V>) is the same logic Phase B’s body emitter encodes.

  • @externalField resolved-reference path → ComputedField shipped end-to-end (computed-field-with-reference, 137f9d2 + 8ca2c78 + 650de56 + 8a6685b): lifts ChildField.ComputedField out of TypeFetcherGenerator.NOT_IMPLEMENTED_REASONS into IMPLEMENTED_LEAVES. @externalField gains a mandatory reference: ExternalCodeReference! argument on the schema directive (matching @service, @tableMethod, @enum); graphql-java rejects no-arg use at parse time, so the classifier never sees a missing-arg case. New ARG_EXTERNAL_FIELD_REF = "reference" constant in BuildContext. ChildField.ComputedField gains a non-null MethodRef method field and implements MethodBackedField; the MethodRef.Basic carries the captured parameterised return type (Field<X>) as a structural TypeName and one Param.Typed at ParamSource.Table for the parent table parameter. New ServiceCatalog.reflectExternalField(className, methodName, parentTableClass) mirrors reflectTableMethod with a stricter contract: must be public static, must take exactly one parameter assignable from the parent’s jOOQ Table<?> class, must return parameterised org.jooq.Field<X> (raw Field is rejected). FieldBuilder’s `@externalField arm now parses the reference, runs an alias-collision check via JooqCatalog.findColumn (rejects when the GraphQL field name shadows a real SQL column on the parent @table), reflects the method, and constructs ComputedField with a populated MethodRef; all resolution failures surface as AUTHOR_ERROR. TypeClassGenerator.emitSelectionSwitch gains a ComputedField arm emitting case "<name>" -> fields.add(<RefClass>.<method>(table).as("<name>")); to inline the developer’s call into the projection list; build$FieldsMethod and buildTypeSpec take a new computedFields parameter so the field actually reaches the switch (the previous flat collection only included ColumnField/NodeIdField/TableField/LookupTableField/NestingField). FetcherEmitter.dataFetcherValue gains a ComputedField arm emitting new ColumnFetcher<>(DSL.field("<name>")), reading by alias from the result Record. TypeFetcherGenerator’s dispatch arm becomes a no-op (wired by `FetcherEmitter, projected by TypeClassGenerator). validateComputedField rejects a non-empty joinPath (lift form) with RejectionKind.DEFERRED until the @reference path lands. Tests across all upper tiers: GraphitronSchemaBuilderTest.ComputedFieldCase extends SCALAR_RETURN to assert the resolved MethodRef shape (className, methodName, single Param.Table) plus new METHOD_NOT_FOUND (reflection failure surfaces AUTHOR_ERROR with the missing-method name) and NAME_COLLIDES_WITH_COLUMN (alias-collision rejection); ComputedFieldValidationTest NO_PATH flips to expect no errors (variant is now implemented), WITH_LIFT_CONDITION expects the new DEFERRED rejection; conflict-test fixtures at GraphitronSchemaBuilderTest:3802/3837 updated with reference: {...} so the now-mandatory schema parses. New test fixture class TestExternalFieldStub provides the reflection target for the schema-builder tests; new fixture class FilmExtensions.isEnglish(Film) -> Field<Boolean> in graphitron-fixtures provides the execution-tier target. New Film.isEnglish: Boolean @externalField(reference: {className, method}) field on graphitron-test/schema.graphqls; new GraphQLQueryTest.films_isEnglish_resolvesViaExternalFieldExpression end-to-end against PostgreSQL via -Plocal-db. Docs: code-generation-triggers.md line 171 reflects the new directive shape and code-emission contract; graphitron-lsp.md Phase 5 dispatch table extended with @externalField reference-argument completion as a tracked deliverable. The legacy no-arg form is not supported; downstream schemas (~49 known instances in Sikt projects) must add reference: { className: "...", method: "..." } when migrating to the rewrite.

  • Apollo Federation 2 entity dispatch via federation-jvm (federation-via-federation-jvm, 0014be7 + c964fc5 + 6898e78 + a200e94 + 55a9b37 + 558abc7 + c643ff6 + 09616d0 + 6e0904e + 040434e + 952a0dd + f35683b + 3cb65d8 + a7e71f4): Query._entities(representations: [_Any!]!): [_Entity]! now resolves natively for every type Graphitron classifies, with no per-consumer wiring beyond the existing Graphitron.buildSchema(...) call. Classify-time model: new EntityResolution(typeName, table, alternatives, nodeTypeId) sidecar on GraphitronSchema.entitiesByType, populated by a new EntityResolutionBuilder that walks every @key-bearing or @node type after TypeBuilder / FieldBuilder. Each resolution carries one or more KeyAlternative(requiredFields, columns, resolvable, KeyShape) entries; KeyShape.NODE_ID is synthesised for every NodeType (decoded via NodeIdEncoder.decodeValues(typeId, id) at runtime), KeyShape.DIRECT is emitted for consumer-declared @key directives (rep field values map index-by-index to column values). @node + explicit @key(fields: "id", ...) dedups by promoting the consumer’s directive while pinning NODE_ID shape so the dispatcher still decodes through NodeIdEncoder rather than treating the literal "id" string as a column value; this preserves the documented resolvable: false opt-out. New FederationKeyFieldsParser rejects nested selections, dotted paths, aliases, arguments, variables, comments, and numeric values with targeted ParseException diagnostics; GraphQLSelectionParser is left untouched. Build-time SDL synthesis: new KeyNodeSynthesiser registry post-step (between FederationLinkApplier and TagApplier in loadAttributedRegistry) attaches @key(fields: "id", resolvable: true) to every @node type that does not already carry an explicit @key(fields: "id", ...), so the supergraph composer sees the entity declaration. Runtime emission: new EntityFetcherDispatchClassGenerator emits an EntityFetcherDispatch class with fetchEntities(env) / resolveByReps(reps, env) / resolveType(env) / typenameForTypeId(typeId). Per-rep flow walks alternatives in most-specific-first order, picks the first resolvable alternative whose requiredFields are all present in the rep, builds a per-rep DFE rebinding arguments to the rep so getTenantId(repEnv) resolves against the individual rep, decodes into a column-value row, and groups bindings by (alternative-index, tenantId) into nested LinkedHashMap`s. Per-group dispatch issues one SELECT per group via a `VALUES (idx, col1, col2, ...) JOIN <table> ORDER BY idx derived table; the idx column carries through SQL so result[row.idx] = r scatters rows back to original federation positions as a SQL property, not a Java post-processing step. Projection includes inline("Foo").as("__typename") plus <TypeName>.$fields(env.getSelectionSet(), table, env); graphql-java’s DataFetchingFieldSelectionSet is type-scoped at the _entities DFE call site, so per-type $fields walks pick up only the inline fragment scoped to each __typename (no cross-type batching needed). QueryNodeFetcher.rowsNodes and fetchById rewired to synthesise {__typename, id} reps and call resolveByReps; the previous per-typeId loop and its canonicalize-encode-scatter round-trip disappear because idx carried through SQL preserves order directly and Base64.getUrlDecoder accepts both padded and unpadded forms. Schema wire-up replaces the placeholder fetchEntities / resolveEntityType lambdas in GraphitronSchemaClassGenerator’s two-arg `build() with EntityFetcherDispatch::fetchEntities / EntityFetcherDispatch::resolveType when entitiesByType is non-empty; otherwise the placeholder lambdas stay so a @link-but-no-entity schema still wraps cleanly. AppliedDirectiveEmitter.emitAstLiteralValue switches from per-scalar enumeration to ValuesResolver.valueToLiteral(...) → AstPrinter.printAst → Parser.parseValue, eliminating a class of latent custom-scalar / Float / input-object / internally-coerced-enum bugs. ColumnRef adds columnClass() accessor used by the dispatcher to type the derived-table Row arity. Federated test fixture (graphitron-test/src/main/resources/graphql/federated-schema.graphqls) is isolated from the shared schema.graphqls via a second graphitron-maven execution generating into no.sikt.graphitron.generated.federated; non-federation tests keep their previous output package. Test coverage across three tiers: 11 EntityResolutionBuilderTest classify-time cases (NODE_ID synthesis with/without explicit @node(typeId:), DIRECT alternatives, multi-key, dedup, compound, resolvable: false carry-through, unresolvable-field demotion, nested-selection rejection, empty-fields rejection, non-@table rejection); 19 FederationKeyFieldsParserTest cases covering naked / braced / mixed whitespace / underscore-and-digit identifiers / commas / nested rejection / unbalanced braces / dotted / aliased / arguments / hash-comments / variables / numeric; 7 FederationBuildSmokeTest cases (two-arg shape, _Service + _entities field present, _Entity union membership, _Service.sdl carries synthesised @key(fields: "id") on every @node type, customizer invocation, one-arg → two-arg delegation); 16 FederationEntitiesDispatchTest end-to-end cases against PostgreSQL (single NODE_ID rep, mixed-typename order preservation, empty representations, unknown __typename, garbage NodeId, DIRECT-shape via filmId, type-scoped selection-set per-type projection, multi-tenancy partition issuing one SELECT per tenant, multi-alternative dispatch per rep, typename-only projection, compound key, compound partial-match yielding null, compound batching one SELECT for multiple reps, customizer-replaces-default no-SELECT-fires, most-specific tie-break selecting compound over simple, non-resolvable @key yielding null without firing SELECT); plus 2 NoFederationRegressionTest cases asserting the shared fixture builds a non-federated schema and emits only the one-arg buildSchema overload. getting-started.md updated: @link intro broadened (a base schema { ... } @link is also accepted), and the two-arg-form example reframed as an escape hatch for entity types Graphitron does not classify (custom fetchers must return jOOQ Record`s with a `__typename column for the default resolveEntityType to recognise them). Hygiene pass shipped alongside: FEDERATION_DIRECTIVE_NAMES moved behind an initialisation-on-demand holder so federation-jvm load failures only surface on schemas that use federation; buildRecipeErrors mixed-error trade-off documented; the federation spec URL lifted from FederationLinkApplier.DEFAULT_FEDERATION_SPEC_URL to a new neutral FederationSpec class in the federation knowledge package (three callers no longer reach into a pipeline class for a constant); unused SchemaDirectiveRegistry.FEDERATION_DIRECTIVES and isFederation deleted (zero production callers; can be brought back if needed); federationLink threaded via a new AttributedRegistry(registry, federationLink) carrier returned from loadAttributedRegistry, so KeyNodeSynthesiser and GraphitronSchemaBuilder.buildBundle no longer re-walk the registry to discover what FederationLinkApplier.apply already determined, and FederationLinkApplier.hasFederationLink deletes. Non-goals: Federation 1, custom resolveEntityType extension point, @interfaceObject, TableInterfaceType as a federation entity, nested-selection @key, build-time _service.sdl artefact emission, cross-__typename SQL union batching, cross-field DataLoader sharing into the entity dispatcher. 909 unit + 23 federation tests green.

  • Set<T> parent-keys on @service methods → MappedBatchLoader (set-parent-keys-on-service, eebf881): extends the BatchKey sealed hierarchy from two to four variants via the cross-product of container axis (List positional vs Set mapped) and key-shape axis (RowN vs RecordN): RowKeyed (existing), RecordKeyed (existing), MappedRowKeyed (new), MappedRecordKeyed (new). keyColumns() lifted to the sealed interface so generator switches can group by shape with multi-pattern arms without re-binding identifiers. ServiceCatalog.classifySourcesType replaces its List.class-only guard with a dual isList/isSet check and picks the variant from the two-axis cross-product; Set<TableRecord> classifies as MappedRowKeyed (matching how List<TableRecord> classifies as RowKeyed). dtoSourcesRejectionReason receives the same dual check so Set<SomePlainClass> now produces "not backed by a jOOQ TableRecord" instead of falling through to the generic "unrecognized sources type" path. TypeFetcherGenerator.buildServiceDataFetcher picks newMappedDataLoader vs newDataLoader from the variant and types the lambda’s keys parameter as Set<KeyType> vs List<KeyType> accordingly; drive-by fix: the existing positional path was calling DataLoaderFactory.newDataLoaderWithContext(...) which does not exist on the API (the split-query path was already correct with newDataLoader). buildServiceRowsMethod returns Map<KeyType, List<Record>> / Map<KeyType, Record> for mapped variants and List<List<Record>> / List<Record> for positional; the data-fetcher return type stays CompletableFuture<V> in all four cases since loader.load(key, env) yields a per-key promise regardless of the underlying batch-loader shape. GeneratorUtils.keyElementType and buildKeyExtraction group by shape via multi-pattern arms. Tests: ServiceCatalogTest gains four reflectServiceMethod_setOf*Sources classification cases (TableRecord, Row1, Record1, DTO-rejection) plus a listOfRecord1 regression; TypeFetcherGeneratorTest gains serviceField_mapped* coverage of the Set/Map shapes and a regression for the newDataLoader fix. Unblocks production schemas that declare @service child fields with Set<SomeRecord> keys.

  • Same-table [ID!] @nodeId filter: primary-key IN predicate (3fdfbfa + 19180ea): a [ID!] @nodeId(typeName: T) field on a @table input type whose T resolves to the input’s own table now classifies as InputField.NodeIdInFilterField and emits NodeIdEncoder.hasIds("typeId", arg, table.col1, ..., table.colN), short-circuiting to DSL.noCondition() when the list is null or empty. BuildContext.classifyInputField adds a same-table guard before findUniqueFkToTable(t, t) (which would always miss for a self-FK lookup) and resolves nodeTypeId / nodeKeyColumns via the same three-tier fallback as NodeIdReferenceField: JooqCatalog.nodeIdMetadata first, then post-first-pass ctx.types, then SDL-only @node with the catalog primary key as a last resort. BodyParam migrates from a single record to a sealed interface with ColumnEq (existing scalar/IN path) and NodeIdIn (new) variants; TypeConditionsGenerator.buildConditionMethod switches on the variant and now takes outputPackage so it can fully-qualify the generated NodeIdEncoder reference. walkInputFieldConditions in FieldBuilder emits BodyParam.NodeIdIn for the new leaf, gated by lookupBoundNames so a future @lookupKey-bound combination still routes through LookupMapping.NodeIdMapping instead. ArgCallEmitter.buildNestedInputFieldExtraction now wraps the leaf cast in List<...> when param.list() is true, fixing the call-site cast for list-shaped filter input fields (the spec assumed this already worked). TypeFetcherGenerator.NOT_DISPATCHED_LEAVES and GraphitronSchemaValidator register the new variant. Tests across three tiers: NodeIdPipelineTest.InputSameTableNodeIdCase (composite-PK, single-PK, target-not-@node Unresolved) using the nodeidfixture catalog because the same-table case requires __NODE_KEY_COLUMNS metadata that Sakila tables lack; TypeConditionsGeneratorTest (single-column, composite-column, list-of-String parameter type, mixed ColumnEq + NodeIdIn); GraphQLQueryTest.films_filteredBySameTableNodeId_* end-to-end against PostgreSQL, asserting both that filtered IDs return exactly those rows and that an empty list passes through to noCondition() returning all rows. VariantCoverageTest.NO_CASE_REQUIRED carries an entry pointing at the pipeline test, parallel to how NodeIdField and NodeIdReferenceField are already handled. Cleanup pass dropped a dead nonNull field on BodyParam.NodeIdIn (the body always guards arg == null || arg.isEmpty() so outer-list nullability is moot).

  • Auto-emit Relay nodes(ids:) resolver when node(id:) exists (auto-nodes-relay-resolver, 71e439f + aa33bd3 + cbbc103 + 40e22b2 + 44d0201 + 6b865f3 + 4aa79f7): new QueryField.QueryNodesField sealed variant routed by FieldBuilder.classifyQueryField for any root-query field named nodes returning [Node] / [Node!] / [Node]! / [Node!]!; GraphitronSchemaValidator adds a no-op arm and TypeFetcherGenerator.buildQueryNodesFetcher emits a thin delegator to QueryNodeFetcher.getNodes parallel to the existing buildQueryNodeFetcher. QueryNodeFetcherClassGenerator extracts the per-typeId dispatch out of getNode into a private fetchById(env, id) helper reused by both single- and batch-paths, then adds a getNodes method that fans ids into per-tenant DataLoader<String, Record>`s keyed by `getTenantId(idEnv) + "/" + path, where idEnv is a per-id DataFetchingEnvironmentImpl.newDataFetchingEnvironment(env).arguments(Map.of("id", id)).build() so apps that vary tenant per id partition correctly (loaders share a registry across the request, so ids resolving to the same tenant batch into one hasIds query while ids from different tenants land in separate loaders; batchEnv.getKeyContextsList().get(0) is safe inside the batch lambda because every key in a given loader shares a tenant by construction). The batch-loader callback rowsNodes(keys, env) groups keys by peekTypeId, runs one dsl.select(...).from(t).where(NodeIdEncoder.hasIds(typeId, typeIds, keyCols)).fetch() per typeId, and scatters rows back to original positions via a Map<String, List<Integer>> keyed by NodeIdEncoder.canonicalize(peekTypeId(id), id) so non-canonical inputs (padded base64, the URL decoder accepts trailing = while encode() emits the no-padding form) still match the canonical encoded id from the result row, eliminating a silent disagreement with node(id:). New NodeIdEncoder.canonicalize(typeId, base64Id) (decode + re-encode, null on malformed input or typeId mismatch) lives next to peekTypeId. The result-scatter projection always appends each nodeKey column to the $fields list (gated by if (!fields.contains(t.<col>)) to dedup against $fields’s `id-driven addition; mirrors TypeClassGenerator’s required-projection-columns pattern) and a synthetic `__typename column so the existing Node TypeResolver still routes by name. GraphitronContext.getTenantId javadoc tightened to spell out the tenant/DSLContext partition contract: when getDslContext varies per id, getTenantId MUST partition by the same key, since the loader picks one DSLContext from keyContextsList().get(0) for the entire batch. Generator-side comment in dispatchNodes documents that the registry is assumed request-scoped (the standard graphql-java pattern; cross-request reuse would let loaders and first-key contexts survive across calls and break tenant scoping). Test coverage: GraphitronSchemaBuilderTest.NODES_QUERY_FIELD classification case, QueryNodeFieldValidationTest no-op case, and 10 execution-tier GraphQLQueryTest cases under "Query.nodes ; Relay batch dispatch" (empty / mixed-type / garbage / unknown-typeId / missing-row / padded-base64 canonicalize regression / duplicate-ids / single-tenant QUERY_COUNT == 2 / per-tenant fan-out QUERY_COUNT == 2 / id-and-other-fields-together asserting both the response id field and the rowsNodes encode read from the same key column). 854 unit + 154 execution tests green.

  • IdReferenceField classifier + synthesis shim (20b3465 + afc11bc + 7fc28fe + a313040 + c594f0a + 37f01fc + ad6303b): new InputField.IdReferenceField sealed variant carries targetTypeName / fkName / qualifier / synthesized describing a filter predicate that resolves to a has<Qualifier>(s) method on the FK source’s jOOQ record class ; the shape KjerneJooqGenerator emits from a single FK out of the input’s resolved table. BuildContext.classifyInputField gains two arms between the existing scalar @nodeId branch and the @reference branch: the canonical form ([ID!] @nodeId(typeName: T) with optional @reference(path: [{key:}]) when the FK is ambiguous) resolves the FK via JooqCatalog.findUniqueFkToTable (new) or the explicit @reference key and emits IdReferenceField with synthesized=false; the synthesis-shim arm placed before column lookup intercepts legacy @field(name: "X_ID") and bare-name forms by reverse-mapping the column name through JooqCatalog.buildQualifierMap (new ; three lowercase keys per FK: raw qualifier, lowerCamel qualifier, plural lowerCamel qualifier; cached per source table) and synthesizes IdReferenceField with synthesized=true plus a per-site WARN whose message names parentTypeName.fieldName and the canonical @nodeId(typeName:) [@reference(path: [{key:}])] replacement that future migration tooling can parse out of build logs. Shim gate is catalog.nodeIdMetadata(targetTable).isPresent() ; the same KjerneJooqGenerator-project sentinel that gates the scalar NodeIdField shim. New catalog helpers: findUniqueFkToTable, buildQualifierMap, qualifierForFk, plus the package-private localGetQualifier reproduction of KjerneJooqGenerator.getQualifier (UpperCamelCase from role + targetTable + "_id"; generateRoleName returns "HAR" when source column equals target column, otherwise the role discriminator). New idreffixture schema (studieprogram + studierett, two FKs: HAR-role on studieprogram_id, role-prefixed registrar_studieprogram whose qualifier RegistrarStudieprogramStudieprogramId deliberately does not match any source column) wired through NodeIdFixtureGenerator.METADATA so the target studieprogram carries __NODE_TYPE_ID. Tests across three tiers: JooqCatalogIdRefTest (22 cases on Sakila + nodeidfixture + idreffixture for findUniqueFkToTable / buildQualifierMap / qualifierForFk / generateRoleName), IdReferenceShimClassificationTest (5 cases ; explicit @field(name:), bare plural, bare scalar, bare id: ID falls through to NodeIdField, role-prefixed where map key ≠ any source column), IdReferenceShimWarnFormatTest (4 cases ; parentType.fieldName format, FK1 + FK2 ambiguous canonical replacements both include @reference, single-FK unique replacement omits @reference), plus GraphitronSchemaBuilderTest.TableInputTypeCase cases on Sakila for canonical-form coverage and matching NodeIdPipelineTest updates. TypeFetcherGenerator.NOT_DISPATCHED_LEAVES registers the new variant; code generation lifts in a follow-up tracked at roadmap/id-reference-input-field.md (Spec). 853 unit tests green.

  • BatchKey.ObjectBased removed (batchkey-remove-objectbased): collapses the BatchKey sealed hierarchy to two variants (RowKeyed, RecordKeyed). ServiceCatalog.classifySourcesType splits the former single Class<?> arm: TableRecord<?> element types now classify as RowKeyed from the parent table’s PK columns via a new SourcesClassification sealed result type; non-TableRecord element types return DtoSourcesUnsupported and surface as UnclassifiedField with an error message naming the field, the sources parameter type, and the batchkey-lifter-directive.md backlog item. GeneratorUtils.keyElementType and buildKeyExtraction ObjectBased switch arms deleted; both switches are now exhaustive over two variants. GraphitronSchemaValidator.validateServiceTableField ObjectBased escape hatch (hasRowOrRecordKeyed early-return) deleted; the parent-table-PK check runs unconditionally. Test coverage: ServiceCatalogTest.tableRecordSources_classifiedAsRowKeyed and dtoSources_rejectedWithLifterDirectiveHint (classifier unit); ServiceFieldValidationTest.OBJECT_BASED rewritten as DTO_SOURCES_REJECTED asserting the rejection path; one pipeline case for the end-to-end DTO rejection. 747 unit tests green; full mvn install -Plocal-db clean.

  • Interface fetchers: selection-set-aware projection (3b982fc): replaces the unconditional table.asterisk() in buildQueryTableInterfaceFieldFetcher and buildTableInterfaceFieldFetcher with a runtime-built LinkedHashSet<Field<?>> fields populated with the discriminator column first (always, regardless of selection set) followed by addAll(<Participant>.$fields(env.getSelectionSet(), table, env)) per ParticipantRef.TableBound. The set deduplicates shared columns (e.g. title declared on both FilmContent and ShortContent collapses to one reference) and preserves insertion order; the .select(new ArrayList<>(fields)) substitution leaves the rest of the DSL chain (.from/.where/.orderBy/.fetch[One]) untouched. New participants: List<ParticipantRef> component on QueryField.QueryTableInterfaceField and ChildField.TableInterfaceField records, threaded by FieldBuilder from TableInterfaceType.participants() at classification time. New TypeFetcherGenerator.buildInterfaceFieldsList helper isolates the field-list emission from both fetcher variants. Six new TypeFetcherGeneratorTest cases (three per fetcher: _noAsterisk_inSelectClause, _discriminatorAlwaysSelected, _participants_emitFieldsCalls); existing tests + two validation tests updated for the record constructor change. 786 unit + 144 execution tests green. Cross-table participant fields (e.g. FilmContent.rating via JOIN to film) carved out as interface-cross-table-participant-fields.md; that follow-up will add the conditional LEFT JOIN gated on env.getSelectionSet().contains("TypeName/fieldName") plus the fixture additions (short_description on content, rating on FilmContent) needed to write its execution-tier tests.

  • runtime-extension-points.md rewritten for the rewrite runtime (13bbbb3 + 72dda8c): replaced the legacy graphitron-common description with the rewrite-emitted contract. The doc now opens with the per-app interface emitted under <outputPackage>.schema.GraphitronContext by GraphitronContextInterfaceGenerator, lists the three actual methods (getDslContext, getContextArgument, getTenantId), and shows the typed-key registration shape (b.put(GraphitronContext.class, ctx)) and the helper env.getGraphQlContext().get(GraphitronContext.class) from TypeFetcherGenerator.buildGraphitronContextHelper. New getTenantId section documents the previously-undocumented contract that Graphitron concatenates getTenantId(env) + "/" + path to build DataLoader registry keys (per TypeFetcherGenerator.buildDataLoaderName); only the tenant prefix is pluggable, the path component is Graphitron-controlled. New "Where each concern belongs" paragraph compares jOOQ Configuration (cross-cutting), getDslContext (per-request), and schema directives (SDL business semantics), absorbing the scope of the deleted graphitroncontext-extension-point-docs.md Backlog item. Wiring example lifted to a pointer at getting-started.md’s Hello World / Tenant-scoped `DSLContext / JWT-claim-context-arguments sections. "Complementary Technologies" coverage of jOOQ Configuration, ExecuteListener, and PostgreSQL RLS preserved. "See also" no longer points at graphitron-common/README.md.

  • Bump generator-side Java floor 21 → 25 (dec71d9): parent pom <release>21</release> → <release>25</release> plus a <requireJavaVersion>25</requireJavaVersion> enforcer rule alongside <requireMavenVersion>3.9</requireMavenVersion>; graphitron-test keeps its <release>17</release> output ratchet (the gap it now covers is "Java-18+ syntax leak" rather than "Java-21+"). Reviewer reproduced: full reactor mvn install -Plocal-db on JDK 25 (BUILD SUCCESS, all modules green); mvn -N validate on JDK 21 fails fast at enforce-versions with "Detected JDK …​ is version 21.0.10 which is not in the allowed range [25,)"; graphitron-lsp.md Phase 6 no longer owns the bump.

  • @asConnection totalCount field (b18b6a0 + 6fdd231): synthesised Connection types now carry totalCount: Int (nullable). ConnectionResult gains table and condition fields populated by the connection fetcher; the existing 2-arg (result, page) convenience constructor threads null, null for the Split-Connection path, and a new 4-arg (result, page, table, condition) constructor is called by buildQueryConnectionFetcher. ConnectionHelperClassGenerator emits a graphitronContext shim (mirroring the per-fetcher convention) and a totalCount(DataFetchingEnvironment) static resolver that runs dsl.selectCount().from(cr.table()).where(cr.condition()).fetchOne(0, Integer.class); graphql-java invokes it only when the client selects the field, so no count SQL is emitted on queries that omit totalCount. FetcherRegistrationsEmitter.connectionBody registers the totalCount coordinate gated on connectionType.schemaType().getFieldDefinition("totalCount"), so synthesised connections always wire it and structural connections wire it only when the SDL author declared the field; the incidental connectionTypeMap projection and unused ConnectionWiring record were removed in the same pass. GraphitronSchemaValidator.validateConnectionType rejects structural totalCount fields whose unwrapped type is not GraphQLInt, using the field’s SourceLocation (falling back to the type location for programmatic schemas) so watch-mode and IDE diagnostics highlight the exact line. Pipeline coverage: GraphitronSchemaBuilderTest.ConnectionTypeCase cases DIRECTIVE_DRIVEN_MINIMAL (synthesised carries nullable Int), STRUCTURAL_CONNECTION (null when absent), STRUCTURAL_CONNECTION_WITH_TOTALCOUNT (structural field preserved); ConnectionRegistrationsTest (synthesised registers, structural-with-Int registers, structural-without does not); ConnectionTypeValidationTest (6 cases covering Int, Int!, absent, String, [Int!]). Execution coverage in GraphQLQueryTest: filtered count equals row-predicate count, synthesised connection count, no select count SQL when field not selected (verified via a jOOQ ExecuteListener that records rendered statements), count SQL issued exactly once when selected. Two Backlog follow-ups filed alongside: totalCount for nested/Split-Connection carriers (returns null until that wiring ships), and count-only execution path (skip page query when only totalCount is selected).

  • @notGenerated directive removed from the supported set: FieldBuilder.classifyField short-circuits any application to UnclassifiedField with reason "`@notGenerated` is no longer supported. Remove the directive; fields must be fully described by the schema." The check runs before detectChildFieldConflict so co-occurring directives don’t shadow the no-longer-supported reason. The directive definition stays in directives.graphqls only so the GraphQL parser doesn’t fail with unknown directive before we emit our error. The NotGeneratedField sealed leaf, its validator dispatch, and the NotGeneratedField filters in TypeFetcherGenerator / FetcherRegistrationsEmitter are deleted. Input-field paths surface the same rejection: BuildContext.classifyInputField short-circuits to InputFieldResolution.Unresolved, which propagates through TypeBuilder.buildTableInputType and the nested-input recursion as an UnclassifiedType reason; FieldBuilder.classifyArgument pre-walks plain-input arg types and emits ArgumentRef.UnclassifiedArg so the surrounding query field becomes UnclassifiedField (necessary because projectFilters only surfaces per-field errors when a @condition / @lookupKey gate fires, so the previously-attempted condErrors entry was dead code). Silent-skip filters in TypeBuilder.buildInputType and the nested-input branch of BuildContext.classifyInputField are removed. Tests updated: NotGeneratedFieldValidationTest deleted; GraphitronSchemaBuilderTest.NotGeneratedFieldCase collapsed into a NOT_GENERATED_DIRECTIVE_REJECTED entry under UnclassifiedFieldCase; NOT_GENERATED_AND_SERVICE_CONFLICT (now subsumed by the short-circuit) deleted; new NOT_GENERATED_REJECTED_PLAIN_INPUT_ARG case under UnclassifiedFieldCase and NOT_GENERATED_REJECTED_TABLE_INPUT / NOT_GENERATED_REJECTED_NESTED_INPUT cases under TableInputTypeCase cover the input-field paths; notGeneratedField_isExcluded / fieldsMethod_excludesNotGeneratedFields deleted (their schemas no longer build). 736 rewrite unit tests green.

  • graphitron-rewrite:watch goal (8ae55b1 + 6bb5419 + review-fix): new WatchMojo in graphitron-rewrite-maven re-runs the rewrite generator on .graphqls changes; composes with content-idempotent writes so only the files whose rendered output actually changed are written and the IDE recompiles only the touched classes. Runs the generator once on startup (skippable via -Dgraphitron.watch.skipInitial=true), resolves the watch directory set from <schemaInputs> parents, and blocks on a SchemaWatcher event loop. SchemaWatcher walks each root recursively at startup and registers newly-created subdirectories on the fly; the Map<WatchKey, Path> registry is ConcurrentHashMap so the watch-loop thread (writes from dispatch on ENTRY_CREATE-for-directory) and the debounce thread (writes from addRoot on re-expanded <schemaInputs>) both touch it safely. Triggers route through a DebounceExecutor (default 300 ms, -Dgraphitron.watch.debounceMs) so a burst of saves coalesces into one regeneration. Validation failures and structural errors are caught and logged with the two-arg getLog().error(msg, throwable) form on both the initial run and watch-loop catch path; the loop survives. JVM shutdown hook closes the WatchService and debounce executor cleanly. Tests at graphitron-rewrite-maven/src/test/java/no/sikt/graphitron/rewrite/maven/watch/: 8 SchemaWatcherTest cases (write, modify, delete, debounce coalescing, non-.graphqls filter, recursive subdirectory registration, OVERFLOW dispatch, addRoot-vs-dispatch registry race) and 2 DebounceExecutorTest cases (burst-coalesces-to-one, close cancels pending). Documentation: new ### Watch mode subsection in graphitron-rewrite/docs/getting-started.md.

  • Service-backed and method-backed root fetchers (c5f8497 + 787a8ae + 8f5ef71 + a0a6319 + b07eec6 + 0730b13 + 7d287f5 + 4d85a3c + 4616b67 + e874b88 + 01b040e + 5b2b87b + 9eae195): closes Stubs #7. QueryTableMethodTableField, QueryServiceTableField, QueryServiceRecordField lift out of TypeFetcherGenerator.NOT_IMPLEMENTED_REASONS into IMPLEMENTED_LEAVES. New ArgCallEmitter.buildMethodBackedCallArgs(MethodRef, CodeBlock, String) walks MethodRef.params() in declaration order with per-ParamSource emission (Arg via the existing extraction switch, Context via getContextArgument, DslContext as literal dsl, Table as the supplied Tables.FOO expression; Sources and SourceTable throw IllegalStateException since the classifier prevents them from reaching the emitter at root). Three new TypeFetcherGenerator per-leaf emitters: buildQueryTableMethodFetcher declares a specific-table local with no cast and projects via <Type>.$fields(...), while buildServiceFetcherCommon (shared between buildQueryServiceTableFetcher and buildQueryServiceRecordFetcher) emits an optional dsl local plus a direct return ServiceClass.method(...) with no projection (graphql-java’s column fetchers walk the records).

    Five classifier-time invariants enforce the strict-typed shape, all surfacing through `+validateUnclassifiedField+` as build-time errors. §1 and §2 share `+FieldBuilder.validateRootServiceInvariants(ServiceResolution)+`, called from both `+classifyQueryField+` and `+classifyMutationField+` so the mutation `+@service+` emitter (still in NOT_IMPLEMENTED_REASONS, lands under Stubs #4) inherits the root-shape constraints when it lifts. §1: Connection wrapper rejected on root `+@service+` / `+@tableMethod+`. §2: `+ParamSource.Sources+` parameter rejected at root (no parent context to batch against). §3: `+@tableMethod+` strict-class equality via `+ClassName.equals+` in `+ServiceCatalog.reflectTableMethod+` (rejects wider `+Table<R>+`); the emitter's no-cast local depends on this guarantee. §4: `+DslContext+` parameter supported only on `+@service+`. §5: strict `+@service+` return type via `+TypeName.equals+` in `+ServiceCatalog.reflectServiceMethod+` against `+FieldBuilder.computeExpectedServiceReturnType(ReturnTypeRef)+` (per-variant table covers `+TableBoundReturnType+` Single/List, `+ResultReturnType+` with non-null `+fqClassName+`, and skips for `+ScalarReturnType+` / `+ResultReturnType+` with null `+fqClassName+` / Connection-wrapped / child `+@service+` with non-empty `+parentPkColumns+`).
    `+MethodRef.Basic.returnType()+` is now a structured javapoet `+TypeName+` captured once via `+TypeName.get(java.lang.reflect.Type)+` at reflection time. Replaces a string-FQCN field plus a `+parseTypeName+` round-trip in `+TypeFetcherGenerator+` (deleted). Comparison is structural so wildcards (`+? extends X+`), array depth, and multi-arg generics participate in equality faithfully; the emitter declares matching fetcher return types directly without parsing strings or widening to `+Object+`. `+ConditionFilter+` overrides `+returnType()+` with a static `+ClassName.get("org.jooq", "Condition")+`. The pre-existing duplicate `+ObjectBased+` branches in `+ServiceCatalog.classifySourcesType+` collapsed to one in passing.
    Test fixture: `+SampleQueryService+` (graphitron-rewrite-fixtures) with `+popularFilms(Film, Double) -> Film+` (filters via `+filmTable.where(...)+`; jOOQ generated tables override every `+where+` / `+as+` / `+rename+` overload to return the specific subtype, so filtering inside `+@tableMethod+` is fully compatible with §3 strict-return), `+filmsByService(DSLContext, List<Integer>) -> Result<FilmRecord>+`, `+filmCount(DSLContext) -> Integer+`. Three SDL Query fields wire them via `+@tableMethod+` / `+@service+` directives.
    Coverage at every tier: 737 unit + 134 test-spec, all green. Three execution-tier cases in `+GraphQLQueryTest+` (filter-and-project with `+QUERY_COUNT == 1+`, service-table column-fetcher round-trip, service-record scalar coercion). Pipeline-tier negative cases in `+GraphitronSchemaBuilderTest.UnclassifiedFieldCase+` cover §1, §2, §3, §5 on both query and mutation arms. Unit-tier cases in `+ServiceCatalogTest+` pin the strict-validation comparison semantics (matching, mismatched raw class, mismatched inner generic, mismatched cardinality, null-expected, table-method matching/mismatched/wider/null). End-to-end `+ServiceRootFetcherPipelineTest+` asserts rejections surface as `+ValidationError+` through the full SDL → classifier → validator path.
      The "load-bearing classifier guarantees → tight emitter code" pattern (compile-time failure of the generated `+*Fetchers+` source as the safety net for any classifier/emitter mismatch) is codified in `+rewrite-design-principles.md+` ("Classifier guarantees shape emitter assumptions") with both this plan's `+@tableMethod+` no-cast local and the pre-existing `+ColumnField+` requires-table-parent check as named instances. Roadmap also gained a Backlog item for exploring how to map developer-declared checked exceptions on `+@service+` / `+@tableMethod+` methods to typed GraphQL errors (`+@error+` types, mutation payload error unions).
    - `+@nodeId+` + `+@node+` directive support (`+a6f5a22+` + `+61e4dfe+` + `+09cf758+` + `+d5e0ed4+` + `+f77daf7+` + `+f403565+` + `+0218054+` + `+19916df+`): Relay Global Object Identification, end-to-end. Plan rewritten to lead with semantics ; `+typeId+` is a wire-format contract; `+@node+` requires `+implements Node+`; SDL wins over jOOQ metadata when both speak; PK fallback fills in omitted `+keyColumns+`; metadata-only synthesis fires a deprecation diagnostic at type and field sites until consumers move to declared directives. `+typeId+` uniqueness is validated at classify time with symmetric demotion on collision. `+Query.node(id: ID!)+` lands as a generated `+QueryNodeFetcher+` class next to the per-type `+*Fetchers+` ; switches on the `+typeId+` prefix extracted via `+NodeIdEncoder.peekTypeId+`, projects each branch through the existing `+<TypeName>.$fields(...)+` plus a synthetic `+__typename+` column; a registered `+Node+` `+TypeResolver+` reads `+__typename+` to route the row to the matching concrete `+GraphQLObjectType+`. Encode + decode + WHERE-builder all live on the locally-emitted `+NodeIdEncoder+` (final, static-only ; no override hook); `+LookupValuesJoinEmitter+` switched off `+no.sikt.graphql.NodeIdStrategy+` so the rewrite tree no longer references `+graphitron-common+`. `+ChildField.NodeIdReferenceField+` emits the FK-mirror collapse path (single-hop FK whose target columns positionally match the target NodeType's `+keyColumns+`) ; encodes the parent's FK source columns directly, no JOIN. The legacy reflection machinery (`+PlatformIdField+` records, `+hasPlatformIdAccessors+`, `+platformIdOutputMethodNames+`, `+sqlToAccessorSuffix+`, related tests) is fully deleted. Test fixtures replace the hand-written `+platformidfixture/+` catalog with output from a custom `+NodeIdFixtureGenerator+` (extends the upstream `+org.jooq.codegen.JavaGenerator+`, hard-codes `+__NODE_TYPE_ID+` / `+__NODE_KEY_COLUMNS+` for `+bar+` (composite key) and `+baz+` (single key)) so the classifier is exercised against real generator output. 706 unit tests + 14 maven tests + 128 execution tests green; six `+Query.node+` execution cases cover round-trip, FK-mirror reference round-trip, unknown-typeId-null, garbage-base64-null, valid-prefix-no-row-null. Federation `+_entities+` sharing this dispatch path is superseded by the existing "Apollo Federation via federation-jvm transform" Backlog item. Two follow-ups remain on Cleanup: retire the synthesis shim once consumer SDL migrates, and lift `+NodeIdReferenceField+` into a JOIN-projection form for non-FK-mirror cases.
    - First-class Connection / Edge / PageInfo / PlainObject / Enum variants (`+0aef2c7+` + `+0ecde9d+` + `+237d6d3+` + `+98021043+` + `+476bbee1+` + `+9a80a1d5+` + `+e352b60+`): six-phase pivot to "classifier is authoritative." `+GraphitronType+` sealed hierarchy gains `+ConnectionType+`, `+EdgeType+`, `+PageInfoType+`, `+PlainObjectType+`, and `+EnumType+`, each carrying its `+GraphQLNamedType schemaType+` populated at classification time for both directive-driven (`+@asConnection+` on a bare list) and structural (hand-written Connection-shaped SDL) paths. `+ConnectionSynthesis+` (385 lines + 243-line test) deleted; `+ObjectTypeGenerator+`, `+EnumTypeGenerator+`, `+InputTypeGenerator+`, and `+GraphitronSchemaClassGenerator+` iterate `+schema.types()+` exclusively, with no `+assembled.getAllTypesAsList()+` fallback loops and no `+hasAppliedDirective("asConnection")+` probes at emit time. `+GraphitronSchemaBuilder.rebuildAssembledForConnections+` performs a two-step rebuild: `+GraphQLSchema.newSchema(existing).additionalType(...)+` registers synthesised Connection/Edge/PageInfo types, then `+SchemaTransformer+` rewrites `+@asConnection+` carrier fields (bare-list return type to Connection `+typeRef+`, appended `+first+` / `+after+` arguments) against the updated schema, so `+assembled.getType("QueryStoresConnection")+` resolves and the assembled schema agrees with the model. `+FieldWrapper.Connection+` shrinks to `+(connectionNullable, defaultPageSize)+` per-site metadata; per-type metadata lives on `+ConnectionType+`. Phase 7 (common `+schemaType()+` accessor) skipped with documented rationale: five variants carry the field but consumers are specialised switches; lifting an accessor would force ~15 unrelated domain variants to carry an unused `+GraphQLNamedType+` for the payoff of removing ~7 `+instanceof+` lines. One latent bug surfaced by Phase 6's enum flip: `+FieldBuilder.classifyArgument+`'s loose `+ctx.types.containsKey(typeName)+` guard misfired on enum-typed arguments once enums entered the model; tightened to `+instanceof InputType || (UnclassifiedType && GraphQLInputObjectType)+`. `+InputDirectiveInputTypes.NAMES+` (`+ErrorHandler+`, `+ReferencesForType+`, `+FieldSort+`, `+ExternalCodeReference+`, `+ReferenceElement+`) skipped at classify time so they never enter `+schema.types()+`; `+_+`-prefix guard moved above the `+GraphQLEnumType+` branch in `+TypeBuilder.classifyType+`. Coverage: six `+ConnectionTypeCase+` classification tests, `+connectionType_directVariant_emitsFieldsFromSchemaType+` in `+ObjectTypeGeneratorTest+` (constructs `+ConnectionType+` / `+EdgeType+` records directly so an emitter bug cannot be masked by classification), `+VariantCoverageTest+` cases for the new variants, snapshot diffs on the test-spec `+schema/+` output (zero diff on Phases 4 / 6; Phase 5 expected drift from the assembled rebuild). Supersedes the `+ConnectionSynthesis+` entry below; the totalCount entry above builds on `+connectionType.schemaType()+`.
    - `+79af12c+` ; Rewrite owns `+@asConnection+` via emit-time synthesis: `+ConnectionSynthesis.buildPlan()+` scans the assembled `+GraphQLSchema+` for `+@asConnection+` on bare-list fields and produces a `+Plan+` (connection name to `+ConnectionDef+` map, `+needPageInfo+` flag) without touching the registry. `+emitSupportingTypes()+` turns the plan into sorted `+TypeSpec+` lists: `+<ConnName>Type+` and `+<ConnName>EdgeType+` each carry `+type()+` + `+registerFetchers()+` (bound to `+ConnectionHelper+`), and `+PageInfoType+` is synthesised when absent. `+ObjectTypeGenerator.buildFieldDefinition()+` rewrites directive-driven fields: replaces the bare-list return type with a `+typeRef+` to the synthesised Connection name and appends `+first: Int = <default>+` / `+after: String+` arguments. `+GraphitronSchemaClassGenerator.generate()+` wires synthesised Connection/Edge/PageInfo types into `+GraphQLSchema.build()+` via `+.additionalType(...)+`. `+GraphQLRewriteGenerator.runPipeline()+` emits the synthesised `+TypeSpec+` files to the schema sub-package. Fixture adds `+stores: [Store!]! @asConnection+` producing `+QueryStoresConnectionType+` + `+QueryStoresEdgeType+`; smoke test verifies both are loadable; two execution tests cover cursor pagination round-trip over the Sakila stores. Structural (hand-written) Connection types are unaffected. 122 pipeline/execution tests green; 32 new unit tests across `+ConnectionSynthesisTest+`, `+ObjectTypeGeneratorTest+`, `+GraphitronSchemaClassGeneratorTest+`.
    - Content-idempotent writes + stale-file sweep (`+5c780fb+` + `+9526217+` + `+84b0af7+`): `+GraphQLRewriteGenerator.write()+` switched from `+writeTo(File)+` (void, always-overwrite) to `+writeToPath(Path, StandardCharsets.UTF_8)+`, which skips disk writes when a SHA-256 comparison against the existing file matches (logic lives in the forked `+no.sikt.graphitron.javapoet.JavaFile+`). Each emitted `+Path+` is collected into a `+Set<Path> emittedThisRun+`; `+sweepOrphans()+` walks the six owned sub-packages non-recursively (`+""+` / `+util+` / `+schema+` / `+types+` / `+conditions+` / `+fetchers+` under `+outputDirectory+`), deletes any `+*.java+` file not in the set, and leaves everything outside those sub-packages alone. Ratchets: pipeline-tier `+GeneratorDeterminismTest+` in `+graphitron-rewrite-test+` runs the full generator against the 448-line fixture schema twice (once into two different output dirs, asserting byte-identical trees; once against the same output dir, asserting mtimes preserved); writer-tier `+IdempotentWriterTest+` in `+graphitron-rewrite+` covers tamper-detection, orphan sweep inside owned sub-packages, and scope preservation outside owned sub-packages against a trivial two-type SDL. Docs: new `+## Dev loop+` section in `+graphitron-rewrite/docs/getting-started.md+` documents the three-clause contract (determinism, minimal-change writes, clean removal) in developer-observable terms plus IntelliJ / Quarkus / Spring Boot DevTools interop. Determinism audit (grep) came back clean: zero `+System.currentTimeMillis+` / `+Instant.now+` / `+UUID.randomUUID+` / `+System.nanoTime+` in generator source, zero `+hashCode+`-keyed comparators, `+fetcherBodies+` uses `+TreeMap+` (stable ordering), one bare `+HashMap+` in `+JoinPathEmitter.generateAliases+` but it's a counter that is never iterated. Legacy-coexistence risk audit: rewrite-test migrated to `+graphitron-rewrite-maven+` during the Maven-plugin landing so no in-repo consumer has both generators active; external-consumer collision is the caller's audit to perform against their own `+<outputPackage>+` layout.
    - Self-contained rewrite aggregator build (`+7df7638+` + `+aa0f0b7+` + `+7da16e7+`): `+mvn install -f graphitron-rewrite/pom.xml+` on a clean empty local repo builds all five rewrite modules without resolving any legacy `+graphitron-*+` artifact. `+7df7638+` dropped `+<module>graphitron-rewrite</module>+` from the root reactor; `+aa0f0b7+` reparented `+graphitron-rewrite-parent+` off `+graphitron-parent+` with inlined dependencyManagement / pluginManagement / compiler (release=21) / enforcer / quick-profile blocks, and replaced `+${revision}${changelist}+` with hardcoded `+9-SNAPSHOT+` across the rewrite tree (sign-off accepted; rewrite-tree bumps are now a five-pom grep-replace). `+7da16e7+` forked `+graphitron-javapoet+` into `+graphitron-rewrite/graphitron-javapoet/+` under coord `+no.sikt:graphitron-rewrite-javapoet+` (package unchanged so rewrite-core imports are untouched; legacy copy byte-identical); swapped rewrite-main's dep; dropped a dead `+graphitron-common+` compile dep from `+graphitron-rewrite-test+` (no Java imports resolved through it) and replaced its transitive `+graphql-java+` path with an explicit test-scope dep on `+rewrite-test+`; shipped `+graphitron-rewrite/scripts/verify-standalone-build.sh+` that runs the aggregator against a fresh empty `+mktemp -d+` local repo and greps the resulting repo for forbidden coords (`+graphitron-common+`, `+graphitron-java-codegen+`, `+graphitron-maven-plugin+`, `+graphitron-schema-transform+`, legacy `+graphitron-javapoet+`); updated `+claude-code-web-environment.md+`, `+rewrite-design-principles.md+`, and root `+README.md+` to name the aggregator-local entry point. Absorbs the former Cleanup-section entry "Drop `+graphitron-common+` build dependency from `+graphitron-rewrite+`" (entry deleted from Cleanup). 695 rewrite-core unit tests green, 116 execution-tier tests green, 2 Invoker ITs green, legacy root reactor byte-identical.
    - Rewrite owns schema loading + directive auto-injection (`+c31771d+`): `+RewriteSchemaLoader+` at `+no.sikt.graphitron.rewrite.schema+` parses user schema paths via `+MultiSourceReader+` with auto-injection of a rewrite-local `+directives.graphqls+` (292-line copy of the canonical from `+graphitron-common+`). Filesystem-only for user sources; `+SchemaParser.buildRegistry+` over `+MultiSourceReader+` with `+trackData(true)+`. Switches `+GraphQLRewriteGenerator+` and `+TestSchemaHelper+` off `+SchemaReadingHelper+`; drops `+graphitron-common+` build dep from `+graphitron-rewrite/graphitron-rewrite/pom.xml+` (declares `+graphql-java+` directly). Consumer-pom fix: `+graphitron-rewrite-test+` dropped its `+<transform>+` execution (which embedded directive declarations in the assembled schema, clashing on parse with auto-injection) and pointed `+<schemaFiles>+` at the raw user schema. `+RewriteSchemaLoaderTest+` covers: two-file fixture load, `+@table+` auto-injection proof, missing-source error, and reader-close verification. Absorbs the Cleanup entry "Drop `+graphitron-common+` build dependency from `+graphitron-rewrite+`".
    - Rewrite-owned Maven plugin (`+76754b3+` + `+8a8c5ef+` + `+17504dd+` + review-round-2 `+6026b98+` + `+388065b+`): new `+graphitron-rewrite-maven+` module with `+GenerateMojo+` / `+ValidateMojo+` driven by `+AbstractRewriteMojo+` (5 `+@Parameter+` fields post-cleanup: `+schemaInputs+`, `+outputDirectory+`, `+outputPackage+`, `+jooqPackage+`, `+namedReferences+`), `+SchemaInputExpander+` (glob expansion via Plexus `+DirectoryScanner+`, fail-fast on zero matches, `+RuntimeException+`-wide catch), and `+RewriteContext+` defensive-copy record. `+graphitron-rewrite-test/pom.xml+` migrated off the legacy plugin; `+enableRewrite+`/`+disableLegacy+`/`+failOnRewriteValidationError+` flags removed. 14 unit tests (GenerateMojoTest, SchemaInputExpanderTest, RewriteContextTest) and 2 Maven Invoker ITs (`+basic-generate+` happy path, `+missing-schema-inputs+` fail-fast). CI-friendly parent POM antrun workaround documented in plugin pom. Review-round 2 cuts (`+6026b98+`): `+<scalars>+` / `+<maxAllowedPageSize>+` excised (both were silent-no-op on the config surface with zero consumers in rewrite core); `+<outputDirectory>+` normalised against `+project.basedir+` instead of CWD; `+mvn graphitron-rewrite:validate+` works standalone from the CLI (validate-only path substitutes an inert package sentinel so the classifier type-checks); `+AbstractRewriteMojo.runGenerator+` unifies the `+RuntimeException+` → `+MojoExecutionException+` wrap so both Mojos share one error envelope. Generator cleanup (`+388065b+`): `+GraphQLRewriteGenerator+` extracts `+logWarnings+` and `+validateAndLogErrors+` helpers, drops stale legacy-Mojo javadoc on the instance ctor.
    - Rewrite owns tagged schema inputs + description notes (`+84cfd644+` + `+8adaaa5e+` + `+a937d2d1+`): introduces `+SchemaInput+` record (sourceName + optional tag + optional descriptionNote), `+SchemaInputAttribution+` with fail-fast overlap check, and a `+RewriteContext+` record carrying `+schemaInputs+` + `+basedir+`. `+TagApplier+` applies `+@tag(name: "<tag>")+` to fields / input fields / enum values / arguments / unions (legacy parity), auto-injecting the Apollo-federation-compatible `+@tag+` directive declaration when the registry has none and skipping elements that already declare `+@tag+`. `+DescriptionNoteApplier+` applies a blank-line-separated note (platform-stable literal `+\n\n+`) to everything `+TagApplier+` touches plus the type declarations themselves per D2 (widened past legacy for object / interface / enum / input). `+GraphQLRewriteGenerator+` gains an instance `+run()+` entry point layering the appliers between loader and classifier; static `+generate()+` stays intact so the legacy Mojo keeps driving `+graphitron-rewrite-test+`. D2 resolved as "widen notes, keep tags narrow"; naming deviation from plan (instance method `+run()+` not `+generate()+`) because Java forbids static + instance overload on one signature ; Maven-plugin plan unifies onto one name when the static retires. Review-round 1 (`+8adaaa5e+`) surfaced two latent production bugs: `+ObjectTypeDefinition.transform()+` on an `+ObjectTypeExtensionDefinition+` returns a plain base definition (fixed by adding extension arms to each applier's switch calling `+transformExtension(...)+`), and `+MultiSourceReader+`'s line-terminator-based source-name tracking bleeds the last line of an unterminated input into the next source (fixed in `+RewriteSchemaLoader+` with a `+terminated()+` Reader wrapper that emits a final `+\n+` only when the inner stream did not). Review-round 2 (`+a937d2d1+`) pinned both fixes: `+RewriteSchemaLoaderTest.unterminatedFirstSourceDoesNotBleedSourceNameIntoSecond+` ratchet with raw-string fixture; four extension tests per applier (Interface / InputObject / Enum / Union mirroring the original Object case); and an F3 follow-up that suppresses the synthetic `+\n+` when the source already ends with one, so `+SourceLocation.line+` in parse-error diagnostics is not shifted by a synthetic trailing blank. Tests: 695 rewrite-core green (from 653 pre-landing; +31 new in the applier + pipeline suites, +9 in the review-round-2 pin, +2 in the latent-bug surfaces).
    - Graphitron emits a prebuilt programmatic `+GraphQLSchema+` (`+81fa607+` + `+5b4ecce+` -> `+4088cb1+` + `+dabfba3+` + `+9b4622e+`): three-commit replacement of the emitted `+Graphitron.java+` facade's SDL + `+RuntimeWiring+` assembly with a single `+buildSchema(Consumer<GraphQLSchema.Builder>)+` call that returns a fully wired schema. Commit A retargets `+GraphitronContext+` into `+<outputPackage>.rewrite.schema.GraphitronContext+` and switches the `+graphQLContext+` key from `+"graphitronContext"+` to `+GraphitronContext.class+`. Commit B lands new `+<TypeName>Type+` generators (enum / input / object / interface / union) in `+<outputPackage>.rewrite.schema+`, a `+GraphitronSchema+` assembler owning the shared `+GraphQLCodeRegistry.Builder+`, the new `+Graphitron+` facade, survivor-directive definitions via `+additionalDirective(...)+` + applications via `+AppliedDirectiveEmitter+` on every type / field / argument / input-field / enum-value builder, default-value round-trip via `+.defaultValueProgrammatic(...)+`, and a legacy-wiring bridge that keeps old emitters live during the transition. Commit C deletes `+WiringClassGenerator+`, `+GraphitronWiringClassGenerator+`, the legacy `+<TypeName>Wiring+` classes, and the `+GraphitronWiring+` aggregator; the bridge is replaced by a new `+FetcherRegistrationsEmitter+` that emits `+codeRegistry.dataFetcher(FieldCoordinates.coordinates(type, field), value)+` bodies directly into the `+<TypeName>Type.registerFetchers+` method; `+GraphitronSchemaValidator.validateNotGeneratedField+` rejects `+@notGenerated+` with the plan-specified error; `+GeneratedSourcesLintTest.emittedSourcesDoNotImportLegacyRuntimeTypes+` ratchets against FQN imports of `+RuntimeWiring+`, `+TypeRuntimeWiring+`, `+SchemaGenerator+`, `+SchemaReadingHelper+`, and upstream `+no.sikt.graphql.GraphitronContext+`. Three execution-tier fallout fixes landed with C: typed `+(DataFetchingEnvironment env)+` lambda params disambiguating the `+DataFetcher+` / `+DataFetcherFactory+` overloads on `+GraphQLCodeRegistry.Builder.dataFetcher+`, five `+.additionalType(Scalars.GraphQLInt)+`-and-friends calls in `+GraphitronSchema.build+` (programmatic schema doesn't auto-register built-in scalars the way `+SchemaGenerator+` does for SDL), and `+.value(name)+` alongside `+.name(name)+` on every enum value so graphql-java's Coercing layer doesn't reject string-matching-enum-name serializations. `+graphitron-rewrite/docs/getting-started.md+` ships alongside covering the five API-quality-gate cases (hello world, custom scalar, federation, tenant-scoped `+DSLContext+`, context arguments from a JWT claim). 649 rewrite unit tests green; 116 execution-tier tests green against the new `+Graphitron.buildSchema+` wiring.
    - `+96e39df+` ; Implicit column conditions for `+@table+` input types: `+FieldBuilder.walkInputFieldConditions+` carries `+enclosingOverride+`, `+lookupBoundNames+`, and a nullable `+implicitBodyParams+` output; every un-annotated `+ColumnField+` / `+ColumnReferenceField+` on a `+TableInputArg+` that is not `+@lookupKey+`-bound and not under an override emits a `+BodyParam+` with `+NestedInputField+` extraction, folded into the same `+GeneratedConditionFilter+` as column-bound scalars. `+projectFilters+` seeds the override flag from parent-field-level and arg-level `+@condition(override:true)+`; plain inputs pass `+null+` to keep legacy "explicit-only" semantics. `+FieldBuilder.javaTypeFor+` drops its `+IllegalStateException+` guard for `+NestedInputField+` now that the implicit path produces column-bound body params; `+implicitBodyParam+` uses `+String+` for `+ID+`-typed fields so `+DSL.val+` coerces at the generated call site. Pipeline: five `+GraphitronSchemaBuilderTest+` cases (bodyparam emitted, explicit-override-suppresses-own, explicit-suppresses-implicit, lookup-key-skipped, nested-two-level). Execution: five `+GraphQLQueryTest+` cases (filtersByColumn, nullField, parentFieldOverride, twoFields AND, nested two-level). `+PlatformIdField+` is intentionally skipped here; the now-shipped `+@nodeId+` + `+@node+` directive support replaces it with a synthesized `+NodeId+` and absorbs the implicit-`+@nodeId+` case under the same path.
    - Argument-resolution unification, Phase 4 (`+9cf83463+` + `+11dc670a+` + `+745a2a15+`): `+@condition+` on `+INPUT_FIELD_DEFINITION+`. `+InputField+` variants carry `+Optional<ArgConditionRef> condition+`; `+ArgumentRef.TableInputArg+` / `+PlainInputArg+` carry `+List<InputField> fields+`. `+BuildContext.classifyInputField+` + `+readConditionDirective+` host the shared classifier invoked from `+TypeBuilder+` (type-build time) and `+FieldBuilder.classifyPlainInputFields+` (per call site). `+FieldBuilder.walkInputFieldConditions+` walks classified fields and, via `+rewrapForNested+`, rebuilds each `+ConditionFilter+`'s `+ParamSource.Arg+` params against a new `+CallSiteExtraction.NestedInputField(outerArgName, path)+` variant; `+ArgCallEmitter+` emits a null-safe `+instanceof Map<?, ?>+` ternary chain from the top-level arg down to the leaf. Six execution tests cover single-level / override / outer-override / nested / plain / plain-outer-override shapes; `+filmsOuterOverrideTableInput+` and `+filmsOuterOverridePlainInput+` are divergence-pins against legacy's "outer owns everything" semantics. Auto-column binding for `+@table+` input types (63 alf call sites) spun out as its own Active plan; the enclosingOverride accumulator lands with it. Plan promoted to design doc on Done: link:../argument-resolution.md[argument-resolution.md].
    - Per-type `+*Wiring+` classes (`+cadab36+` + `+2c366bb+`): `+WiringClassGenerator+` at `+no.sikt.graphitron.rewrite.generators+` emits one `+<TypeName>Wiring+` class per GraphQL type to `+<outputPackage>.rewrite.wiring+`, covering five categories (regular, nested with `+BatchKeyField+` leaves, nested without, Connection, Edge); `+ConnectionWiring+` / `+NestedTypeWiring+` are private records inside the generator and the public entry is schema-only (`+generate(GraphitronSchema)+`). `+TypeFetcherGenerator+` lost `+wiring()+`, `+emitWiring+`, `+buildWiringEntry+`, `+buildPropertyOrRecordFetcherEntry+`, `+buildWiringMethod+`; `+GraphitronWiringClassGenerator+` shrank to a pure aggregator (`+.type(XxxWiring.wiring())+` per class name, alphabetically sorted). Lint ratchet `+GeneratedSourcesLintTest.wiringAggregatorDoesNotInlineTypeWiring+` pins `+GraphitronWiring.java+` free of any `+newTypeWiring(+` call so future categories can't quietly re-inline. Follow-up `+2c366bb+` fixed five raw-type warnings surfaced by the refactor (threading `+ParameterizedTypeName+` + `+WildcardTypeName+` through `+$T+` substitution and broadening two `+@SuppressWarnings+`) and added two `+[Backlog]+` Cleanup items (PageInfo wiring decision, `+TypeResolver+` wiring for interface/union).
    - `+89dfea8+` ; `+DSLContext+` params on `+@service+` methods: `+ServiceCatalog.reflectServiceMethod+` classifies `+org.jooq.DSLContext+` parameters as `+ParamSource.DslContext+`; four `+ServiceCatalogTest+` cases + one `+GraphitronSchemaBuilderTest+` pipeline case. `+reflectTableMethod+` intentionally unchanged ; tracked as backlog.
    - `+3357928+` ; Sealed-switch dispatch: `+TypeFetcherGenerator.generateTypeSpec+` exhaustive over all `+GraphitronField+` leaves; stubbed leaves via `+NOT_IMPLEMENTED_REASONS+`.
    - `+15f9f61e+` ; Variant-coverage Phase 1: `+IMPLEMENTED_LEAVES+` / `+NOT_DISPATCHED_LEAVES+` partition invariant enforced by `+GeneratorCoverageTest+`.
    - `+1e48c4ee+` ; Argument-resolution Phase 1: VALUES + JOIN lookup emission for `+QueryLookupTableField+`.
    - G5 ; Inline `+TableField+` emission: `+TypeClassGenerator.$fields+` via `+DSL.multiset+`; seven execution tests.
    - `+aaadb78b+` ; Argument-resolution Phase 2a: inline `+LookupTableField+` via `+InlineLookupTableFieldEmitter+`; six execution tests.
    - `+7417f53+` ; Body-substring test rewrite: `+TypeSpecAssertions+` helper; 28 → 3 intentionally-marked body-assertion sites.
    - `+34359b4+` ; Argument-resolution Phase 2b: rows-method bodies for `+SplitTableField+` + `+SplitLookupTableField+`; exact JDBC round-trip counts asserted.
    - Record-fields Phase 1: `+ResultType+` parents; `+PropertyField+`, `+RecordField+`, `+ConstructorField+`, `+RecordTableField+` with execution tests.
    - Record-fields Phase 2: `+RecordLookupTableField+` via `+deriveBatchKeyForResultType+`; five execution tests.
    - `+9ba498bc+` + `+7cf568f4+` ; Stubbed-variant validator: `+validateVariantIsImplemented+` reads `+NOT_IMPLEMENTED_REASONS+`; build fails on rewrite validation errors by default.
    - `+@table+` + `+@record+` input-type fix: `+@record+` dominates on input types; introduces `+BuildContext.warnings()+` channel.
    - `+d33ace9+` ; Variant-coverage Phase 2: `+ClassificationCase+` interface; 26 enums retrofitted with `+variants()+` sets.
    - Java-17 output ratchet: `+graphitron-rewrite-test+` compile goal pinned to `+release=17+`.
    - Consolidate rewrite modules under `+graphitron-rewrite/+` shipped at `+0e5eb86+`.
    - `+0b2e4e9+` + `+49d7879+` ; Nesting-field emission: `+ChildField.NestingField+` out of stubs; eight execution tests.
    - `+1abc31ed+` + `+0c449fef+` + `+a3afd651+` ; Implicit `+@reference+` path inference: `+BuildContext.parsePath+` synthesizes single-hop `+FkJoin+` from the jOOQ catalog when `+@reference+` is absent; deletes four `+SplitRowsMethodEmitter+` EMPTY_PATH stub branches and the duplicate FK-count logic in `+GraphitronSchemaValidator+`.
    - `+2530b93+` + `+f8df839+` + `+a063d3e+` + `+ef89bfb+` + `+1900453+` ; Generated-fetcher quality pass: `+ConnectionHelper.pageRequest+` + emitted `+PageRequest+` carrier own the full pagination dance (first/last guard, backward/pageSize/cursor derivation, cursor decode, reverse ordering, selection ∪ extraFields name-dedup), with `+reverseOrderBy+` lifted from per-`+*Fetchers+`-class to one shared copy; `+QueryConditionsGenerator+` extracts env-aware condition orchestration into a parallel generated class so entity `+*Conditions+` stay pure; `+$T+` substitution replaces every `+var+`-emitting site in the generator; table-local rename from `+table+` → `+<entity>Table+` with `+srcAlias+` threaded through `+ArgCallEmitter+` + all `+buildCallArgs+` callers, breaking the mapper/table name collision; `+FieldWrapper.DEFAULT_PAGE_SIZE+` unifies four fallback sites; `+seekFields: Field<?>[]+` matches `+decodeCursor+`'s declared return type; `+ConnectionResult+` gains a 2-arg delegating constructor. Three emitted-source lint ratchets (`+GeneratedSourcesLintTest+`): no `+var+`, no full-package jOOQ qualification in fetcher bodies, no `+graphql.*+` imports in entity `+*Conditions+`. (xref:plans/plan-generated-fetcher-quality.adoc[plan-generated-fetcher-quality.md])
    - `+78e3b7c+` + `+1dce680+` ; `+SplitTableField+` / `+SplitLookupTableField+` under `+NestingField+`: `+GraphitronSchemaValidator.NESTED_WIREABLE_LEAVES+` accepts both `+BatchKeyField+` variants; `+TypeFetcherGenerator.generate+` walks `+NestingField+` descendants of each `+TableBackedType+` root and emits a narrow `+<NestedTypeName>Fetchers+` class (`+emitWiring=false+`) for every nested type with at least one `+BatchKeyField+` leaf ; plain-object nesting types are absent from `+schema.types()+`, so the walk is a second pipeline rather than an extended filter. `+GraphitronWiringClassGenerator+` threads the class name via `+ClassName.get(fetchersPackage, …)+` so `+$L::$L+` emits a proper import; `+GraphQLRewriteGenerator+` filters `+fetcherClassNames+` to TypeSpecs that carry a `+wiring()+` method so the top-level builder doesn't invoke a missing method on nested Fetchers classes. `+TypeClassGenerator.collectBatchKeyColumns+` recurses into `+NestingField.nestedFields()+` so nested Split BatchKey columns land in the outer parent's SELECT. Coverage: `+GraphitronSchemaBuilderTest+` classifier case, `+SplitTableFieldPipelineTest+` + `+NestingFieldPipelineTest+` structural tests (`+TypeSpecAssertions.appendsRequiredColumn+` pins the outer-parent PK projection), 2 execution tests in `+GraphQLQueryTest+` for `+Film.info.cast+` and `+Film.info.castByKey+` each batching two parents into one round-trip. Closes the 12-count production rejection.
    - `+86ff568+` + `+3246fd7+` + `+75e6340+` ; Single-cardinality `+@splitQuery+` support: `+FieldBuilder.deriveSplitQueryBatchKey+` picks FK-column `+BatchKey+` for single cardinality / parent-PK `+BatchKey+` for list (cardinality is the direction signal); classifier rejects `+@splitQuery @lookupKey+` at single and multi-hop single at classifier time; `+SplitRowsMethodEmitter.buildSingleMethod+` emits a flat terminal-JOIN returning `+List<Record>+` with a `+scatterSingleByIdx+` scatter; `+TypeClassGenerator.$fields+` always appends each Split* child's BatchKey columns (deduped at runtime); `+TypeFetcherGenerator+` threads a null-FK short-circuit (single-cardinality fetchers extract the FK to a typed local and return `+CompletableFuture.completedFuture(null)+` before DataLoader dispatch); scatter-helper emission gated so `+scatterByIdx+` / `+scatterSingleByIdx+` are emitted only when the class actually uses them. `+JoinStep.FkJoin+` docstring corrected to describe `+sourceTable+` as the traversal-origin table. Coverage: 4 new `+GraphitronSchemaBuilderTest+` cases (positive + negative §1b / §1c), `+ScatterSingleByIdxTest+` (reflective unit), 3 pipeline tests in `+SplitTableFieldPipelineTest+`, 5 execution tests in `+GraphQLQueryTest+` covering shared-FK dedup (2 round-trips for 5 customers), null-FK short-circuit, non-null-FK resolution, and scatter alignment across mixed-null batches. Closes the 280-count production rejection. (xref:plans/plan-single-cardinality-split-query.adoc[plan-single-cardinality-split-query.md])
    - R15 ; `+f65ad06+` ; Doc-drift sweep: rewrite-internal docs (`+code-generation-triggers.adoc+`, `+rewrite-design-principles.adoc+`, `+argument-resolution.adoc+`) realigned with `+model/+` taxonomy. Generators table restructured into four families (fetcher / schema / error-handling / runtime helpers); `+QueryEntityField+` retired with `+EntityFetcherDispatch+` footnote; `+QueryNodesField+`, `+ChildField.ParticipantColumnReferenceField+`, `+ChildField.ErrorsField+`, the five `+GraphitronType+` permits, the `+CallSiteExtraction+` two sealed sub-groupers, and the `+BatchKey+` two-axis enumeration all surfaced; `+GraphitronSchema+` schematic corrected to all five fields; `+BatchKey.java+` Javadoc updated to "Ten permits across two axis sub-hierarchies". Single R86 forward-ref note added for the typed-rejection / sealed-hierarchies / wire-format-boundary principles slated to consolidate into the public architecture chapter.
    - `+3821842+` + `+62b51c3+` + `+76887cf+` + `+c40afb4+` ; Lift `+@asConnection+` rejection on `+@splitQuery+` fields: `+SplitRowsMethodEmitter.buildConnectionMethod+` emits the `+ROW_NUMBER() OVER (PARTITION BY fk ORDER BY …)+` envelope over a `+parentInput+` VALUES + FK-chain aliased subquery, filtered on outer `+__rn__+` range, so per-parent Relay pagination works inside DataLoader batches; §2 lifts the fixed-ordering restriction by parameterizing `+TypeFetcherGenerator.buildOrderByHelperMethod+` on the aliased `+Table+` so root (`+filmTable+`) and Split (`+a1+`) call sites share one helper shape; helper-emission gate adds `pass:c[SplitTableField+Connection+Argument]` alongside the root-field case; classifier permanently rejects `+@asConnection+` + `+@lookupKey+` at `+FieldBuilder.java:252-257+` / `+:266-271+` (composite lookup keys disambiguate batches, but cursor pagination requires lockstep batches). `+ConnectionResult+` storage narrowed from `+Result<Record>+` to `+List<Record>+`. Coverage: classifier/pipeline/execution tiers all green (545 rewrite + 94 test-spec). Closes the 68-count production rejection.
  • R87 (4867dc0 + d747d93 + b2fcdea + 0ce61c8): @service directives now classify instance methods on (DSLContext) holders, restoring legacy parity. The static/instance fork lives on MethodRef.CallShape; emitter dispatches via serviceCallTarget. MethodRef is sealed with permits NonCondition (which permits Service/StaticOnly) and ConditionFilter. reflectTableMethod carries a positive Modifier.isStatic rejection paired with service-catalog-tablemethod-must-be-static @LoadBearingClassifierCheck. Out-of-band: ServiceHolderFactory extension point not added ; see runtime-extension-points.adoc.

  • R5 (0480a6bd): cleanup-and-hardening pass on the already-shipped composite-key @lookupKey path. LookupMapping.MapInput / DecodedRecord canonical constructors reject empty bindings; three new @LoadBearingClassifierCheck keys (lookup-mapping-bindings-table-coherent, lookup-key-input-field-non-list, lookup-field-non-empty-args) cover the lookup pipeline with matching @DependsOnClassifierCheck consumers on LookupValuesJoinEmitter. New LookupMappingTest pins the type-level invariants; LookupTableFieldPipelineTest extended to assert projected ColumnMapping.MapInput shape; new CompositeKeyLookupQueryTest execution-tier asserts the rendered SQL uses using ("film_id", "actor_id") so single-column regressions surface in test rather than at runtime. The shipped shape is TableInputArg + MapInput, not the ArgumentRef.CompositeLookupArg the original Backlog one-liner anticipated; the unified path was preferable and is locked in by the type-level invariants.

  • R38 (97201f5…ee93207 + 5d82380 + 0839488 + 528fc91 + 50195c7): Reshape BatchKey into SourceKey + unify the rows-method seam. Ten BatchKey permits collapse to SourceKey (flat record carrying target, columns, path, wrap, cardinality, reader) + LoaderRegistration (container × dispatch); the rows-method seam routes through one entry point per concern: RowsMethodSkeleton.build (declaration scaffolding + body framing), RowsMethodCall.batchLoaderLambda (BatchLoader lambda), DataLoaderFetcherEmitter.build (DataFetcher dance). Three source-key.* @LoadBearingClassifierCheck keys (SourceRowsCall⇒Row, AccessorCall⇒Record, ServiceTableRecord target-aligned⇒empty path) paired with consumers in GeneratorUtils + SplitRowsMethodEmitter. UnifiedEmissionPinsTest pins the three-fetcher / four-rows-method routing structurally. Net type-identity count: from 10+ permits down to 1 SourceKey + 5 Reader sub-permits + 1 LoaderRegistration. The "Sealed hierarchies over enums" worked example lifted out of the principles doc to a new sibling page graphitron-rewrite/docs/dispatch-axes.adoc. R75’s ResultRowWalk Reader permit will land as a one-permit addition on this foundation.

  • R18 (21c5e57 + c699979…f228556 + 2a86e5e + a76383b…2a7b3ba + 035ef2b + ed5ebf3 + 39ca34f + a672c82 + bec04f8 + 50dbcdc + 232f8e0 + 0bbd6f3 + 9f41cdc + 5c9109d): Java LSP rewrite + dev goal. Replaces the Rust graphitron-lsp and the legacy graphitron-maven-plugin:introspect JSON producer with a Java LSP module under graphitron-rewrite/graphitron-lsp, served by the single mvn graphitron:dev goal binding 127.0.0.1:8487. Phases 0–6 delivered: lsp4j scaffold (Phase 0), dev-goal binding + watchers + UTF-8↔UTF-16 position conversion (Phase 1), in-process GraphQLRewriteGenerator.buildCatalog() returning tables / columns / FKs / scalars (Phase 2), per-directive completion + diagnostics + Markdown hover for @field / @reference (Phase 3), goto-definition into the jOOQ-generated source tree (Phase 4), @service / @condition / @record autocomplete + hover + diagnostics off a JDK 25 java.lang.classfile-driven scan including the Phase 5d directive-shape correction (descend through the outer service: / condition: / record: arg into the nested ExternalCodeReference) and Phase 5e multi-module reactor visibility (MavenSession.getAllProjects() → every reactor’s compile-output directory; RewriteContext.classpathRoots field + six-arg back-compat overload), and -parameters-missing detection via Parameter.name == null (Phase 5a–5e), and the bonede→jtreesitter binding swap with vendored tree-sitter-graphql grammar source plus per-platform native build (Phase 6). Phase 7 (Rust archival, IntrospectMojo deletion, consumer migration docs) carved out into R91; Javadoc surfacing + per-line definitions + @externalField walk + argMapping autocomplete deferred into R90; multi-platform native CI tracked under R89. 90+ LSP + 48 graphitron-maven module tests green; full reactor build green including graphitron-sakila-example against the new instance-@service fixture from R87.

  • R134 (36122dc + 7fadbda): Fix mutation empty-input short-circuit to use newRecord for single-record payloads. TypeFetcherGenerator.buildMutationDmlRecordFetcher branches the empty-list arm on dataIsList: DSL.using(...).newResult(<pkProjection>) for the projected-list arm, DSL.using(...).newRecord(<pkProjection>) for the single-record arm (mutations whose direct return is a single payload, e.g. opprettX(input: [XInput]): XPayload!). The non-empty branch was already gated on dataIsList via fetch()/fetchOne(); this aligns the empty arm. Regression coverage is owned by the compilation tier: graphitron-sakila-example/schema.graphqls declares createFilmsPayload(in: [FilmCreateInput!]!): FilmPayload @mutation(typeName: INSERT), the exact bulk-input + single-payload shape that triggered the bug. The generated MutationFetchers.createFilmsPayload emits Record1<Integer> payload = DSL.using(dsl.configuration()).newRecord(Tables.FILM.FILM_ID) and is compiled against real jOOQ classes; a regression to newResult(...) would re-emit Result<Record> into a Record1<Integer> local and fail compilation. Scoped to INSERT because bulk UPDATE/UPSERT on MutationDmlRecordField still throw upstream. Follow-up worth filing separately: the non-empty branch on bulk-input + single-record-payload calls .fetchOne() against multi-row valuesOfRows(...) VALUES, discarding N-1 returned keys at runtime; the compile bug is fixed but the runtime coherence question (validator-side rejection vs. emit lift to .fetch()) deserves its own item.

  • R238 (9451dff…34ebddd + rework d4824e1): ServiceMethodCall walker carrier across the four root sync @service permits (QueryServiceTableField, QueryServiceRecordField, MutationServiceTableField, MutationServiceRecordField). Each permit drops MethodRef method / MethodBackedField and gains a ServiceMethodCall serviceMethodCall slot via the new ServiceField sibling interface; buildServiceFetcherCommon drives generation through ServiceMethodCallEmitter. Lands the walker-carrier plumbing every subsequent slice inherits: WalkerResult<C> sealed wrapper, the ServiceMethodCallError sub-seal of AuthorError, graphitron-side Diagnostic/Severity with the LSP projector at the wire boundary, and the orchestrator’s collect-Err-exclude-field flow (ValidationReport.walkerDiagnostics). ContextArgumentClassifier grows a ServiceField harvest arm; ConflictSite.site widens to a two-arm sealed identifier. Shipped as a translator over a resolved MethodRef.Service rather than fresh SDL+classloader reflection; the substrate absorption, multi-arg ctors, silent-first-match retirement, and the per-arm typed-error taxonomy (10 arms trimmed to the 2 the translator produces, MultipleDslContextSlots + ParameterUnbindable) are carved into R256 (service-walker-substrate-absorption). Full pipeline green end-to-end.

  • R243 (16c85fd + 6fd3878): Per-field direction in @order / @defaultOrder via FieldSort.direction. Lifts the whole-spec OrderBySpec.Fixed.direction: String down onto a per-entry typed ColumnOrderEntry.direction: SortDirection (nested enum carrying jooqMethodName() + flipped()), so a single fixed spec expresses heterogeneous order (rental_rate DESC, title ASC). OrderByResolver reads the directive-level direction: default (ASC when absent on @defaultOrder, ASC for @order), pushes it down per-entry with per-field FieldSort.direction: winning, and precomputes Fixed.uniformAsc once; PK-fallback and index:/primaryKey: synthesised entries stamp SortDirection.ASC explicitly (fork b). Emission at every fixed-spec call site (TypeFetcherGenerator, InlineTableFieldEmitter, SplitRowsMethodEmitter) switches fixed.jooqMethodName() → col.direction().jooqMethodName(). The @orderBy helper bodies dispatch on uniformAsc: uniform-ASC keeps the runtime dir-flips behaviour, mixed is direction-locked (per-entry directions emitted verbatim, runtime dir ignored) ; the opt-out semantics over multiplier, per Stability through simplicity + Generation-thinking. Schema change is purely additive (direction: SortDirection on FieldSort, no default). flipped() retained on the enum per settled note 3 (single-place ASC↔DESC algebra; pinned by the unit test, unused by the build-time emitter). Coverage: unit OrderBySpecSortDirectionTest; pipeline GraphitronSchemaBuilderTest (PER_FIELD_DIRECTION_DEFAULT_ORDER, DIRECTIVE_LEVEL_DIRECTION_PUSHES_DOWN, PER_FIELD_DIRECTION_ORDER_ENUM_VALUE, NO_DEFAULT_ORDER_PK_FALLBACK, DEFAULT_ORDER_DIRECTION_DESC); execution filmsByRateDescTitleAsc_executesHeterogeneousOrder + filmsOrderedConnection_mixedOrderEnumValue_ignoresRuntimeDirection (both ordering on rental_rate DESC, title ASC so the DESC primary and ASC secondary-tiebreak are independently observed). 6fd3878 switched the demonstrators off the seed-uniform release_year onto rental_rate so the primary DESC is no longer a no-op tie. Full reactor green.

  • R258 (ac1eee4): Payload-returning UPDATE onto the UpdateRows carrier. The payload-return UPDATE shapes (updateFilmPayload / updateFilmsPayload) now classify as the new MutationField.MutationUpdatePayloadField / MutationBulkUpdatePayloadField leaves (both UpdateRowsField, non-Optional InputArgRef + UpdateRows slots, no DmlKind discriminator) through FieldBuilder.classifyUpdatePayloadField → UpdateRowsWalker (PK-or-UK matched-key membership), never MutationInputResolver.resolveInput’s `@value partition. With R246’s direct-return path, no UPDATE path reads @value ; the precondition for R188 retiring the directive. The shared record-carrier leaves shrink their live DmlKind range by rejecting UPDATE in their compact constructors (MutationDmlRecordField → {INSERT, UPSERT, DELETE}, MutationBulkDmlRecordField → {INSERT, DELETE}); MutationInputResolver’s UPDATE `@value block becomes a loud IllegalStateException (classifier guarantee made loud). Emit: TypeFetcherGenerator extracts shared buildSingleRecordTwoStepFetcher / buildBulkRecordTwoStepFetcher skeletons parameterized on a carrier-source chain seam (chainFn / perRowBodyFn, not a re-switched DmlKind); buildCarrierUpdateChainSingle / buildCarrierBulkPerRowUpdateBody source SET/WHERE from setGroupsOf(updateRows().setColumns()) / keyGroupsOf(updateRows().keyColumns()), never tia.setFields(). The payload data field’s SingleRecordTableField classification is grounded by the SDL-coordinate-keyed DmlEmitted binding (RecordBindingResolver.groundDmlMutationField + the classifyChildFieldOnResultType unified pass), so no per-field reclassify is needed on the non-DELETE path. FilmUpdateInput drops @value (shared across all four UPDATE mutations). Coverage: SingleRecordPayloadPipelineTest UPDATE arm split onto the new leaves (@value dropped from the UPDATE inputBody, UPSERT kept for R188); GraphitronSchemaBuilderTest truth-table + @condition/UpdateRowsError payload-rejection + DmlRecord projection cases; execution round-trips updateFilmPayload_updatesRowAndReturnsPayloadWithSingleDataField + bulkUpdateWithThreeRowsInNonPkOrderPreservesInputOrderInResponse green after the @value drop. Full reactor green. Unblocks R188.

  • R265 (8586cbd + 53d3da2): Fix non-compiling new GraphqlErrorException(String) in the NodeId ThrowOnMismatch decode helpers. Both throw arms in CompositeDecodeHelperRegistry.buildHelper (scalar :131, reachable; list :113, defensive/unreachable today) switch from the non-existent String constructor to the builder form GraphqlErrorException.newErrorException().message(MISMATCH_MESSAGE).build(), mirroring the four already-correct sites (LookupValuesJoinEmitter, TypeFetcherGenerator ×2, InputBeanInstantiationEmitter). The bug rode in via R260’s lift of NodeId decode out of ArgCallEmitter into the registry; it survived because the existing CompositeDecodeHelperRegistryTest string assertions pin only the FQN graphql.GraphqlErrorException, which renders identically in broken and builder form, and no fixture drives a ThrowOnMismatch arm through javac. The compilation-tier regression guard (Deliverable 2) was deferred to R273: the scalar arm is reachable only via the legacy __NODE_* metadata path (FieldBuilder’s bare-ID` block), so a compile fixture cannot be built without exercising legacy behavior R273 is retiring. FetcherEmitter:284 verified as a correct UnsupportedOperationException site, not a sixth GraphqlErrorException site. No new fixture or assertion; the existing unit pins stay green against the builder form.

  • R244 (622a470 + 0db48be + 999b86e + 845fc25 + 95ae80a + rework 18d46d4): Error-channel slice 1, the typed Outcome<T> transport for root @service outcome fields. Retires the developer-payload construction path (ErrorChannel.PayloadClass, the PayloadConstructionShape family, payloadFactory* / declareEarlyPayload*, ErrorRouter.dispatch) for the four root @service variants, replacing it with a request-time Outcome source the fetcher returns: Success(value) on the happy path, ErrorList(errors) on the mapped-error path, with the unmapped fallback still ErrorRouter.redact. The wrapper resolves the localContext draft’s silent errors-drop by construction (non-null source, so graphql-java always descends into the outcome type’s children). New pieces: the generated Outcome runtime type (OutcomeClassGenerator), the OutcomeType classification, ErrorChannel.Mapped, the output-walking ErrorChannelWalker (R238’s analogue, absorbing the channel-rule + accessor-coverage checks), ChannelCatchArmEmitter / ChannelEarlyReturnEmitter, ChildField.ErrorsField.Transport.WrapperArm, the ErrorChannelWalkerError sub-seal of AuthorError (four arms + LSP graphitron.error-channel.* codes + typed-rejection.adoc), and two classify-time validator rules (MultipleErrorsFields, NonNullableSuccessProjectionField, the latter the load-bearing rail preventing the silent errors-drop). Arm-switch is an explicit generation-time inline read against success.value() (delegation prototype rejected by principles review). The wire shape is pinned by the GraphQLQueryTest execution round-trip (success + mapped-error + unmapped arms), with both rejection rules covered by ErrorChannelClassificationTest.nonNullableSuccessProjectionField_rejectsCarrier and OutcomeTypeValidationTest. @tableMethod + child @service flips and the full PayloadClass delete are deferred to a follow-up slice (the PayloadClass arm stays live for those paths); DML stays on the sentinel/localContext transport (R268 owns the arm-switch machinery retirement; R274 the vestigial OutcomeType.successProjection). Supersedes R241, moots R201. Full pipeline green across all tiers.- R268 (fc365b4 + self-review f4ae047): Collapse the Outcome arm-switch to a binary fork over reused field resolution. R244 slice 1 introduced a second switch over the ChildField taxonomy: FetcherEmitter.armSwitchValueExpr re-derived each variant’s read behind an allow-list (OUTCOME_TYPE_ARM_SWITCHED_DATA_CHANNEL_VARIANTS, nine variants) that drifted from the four the emitter implemented, yielding both a latent IllegalStateException (four allow-listed-but-unimplemented variants) and a false author-error rejection of @table-bound DataLoader data fields (RecordTableField, the opptak-subgraph shape) sibling to the errors field. This retires the parallel taxonomy at the root: the arm-switch now reuses each field’s own read, source-bound from env.getSource() to success.value(). Five seams: FetcherEmitter.dataFetcherValue forks three structural roles under sourceIsOutcome (errors field + method-backed DataLoader fields fall through to the raw method reference; inline-resolved data fields arm-switch in place via armSwitchedInlineDataFetcher/inlineSuccessRead, covering jOOQ-record column get, the shared recordBackedAccessorRead, and constructor/nesting passthrough); GeneratorUtils.buildRecordParentKeyExtraction + the buildFkRowKey/buildLifterRowKey/buildAccessorKey* helpers take a source-binding CodeBlock; DataLoaderFetcherEmitter.build gains a pre-registration-prelude overload so the @table fetcher narrows Success and returns completedFuture(null) on the ErrorList arm before loader registration (the preferred seam-3 ordering); hasWrapperArmErrors hoisted to one home on FetcherEmitter for both FetcherRegistrationsEmitter and TypeFetcherGenerator; and GraphitronSchemaValidator.validateOutcomeChildArmSwitch deletes the allow-list, replacing membership with a contextual structural invariant (every immediate child of a WrapperArm outcome type resolves through a graphitron-emitted fetcher, never graphql-java’s default PropertyDataFetcher) keyed off the shared FetcherEmitter.resolvesViaPropertyDataFetcher. R270 (allow-list/emitter reconcile) is moot. The four nested-method variants and DML stay out of scope; R269 (success-arm null-guard) and R271 (dunder sweep) coordinate on the same helper lines. Coverage: pipeline-tier FetcherPipelineTest.outcomePayload_tableDataField_* + outcomePayload_columnDataField_armSwitchesInlineReadOnSuccessValue (classification + wiring kind, no body-string assertions); validation-tier OutcomeTypeValidationTest.outcomePayloadWithTableDataField_isNotRejected (false-rejection fix with classification preconditions); execution-tier GraphQLQueryTest.submitFilmReviewWithFilm_* (new sakila FilmReviewWithFilmPayload fixture round-tripping both arms). Full reactor green.

  • R283 (f55b9ac): Emit the @oneOf directive definition into the federation SDL outputs. ServiceSDLPrinter.generateServiceSDLV2 prints the @oneOf application but strips the spec-built-in definition (DirectiveInfo.isGraphqlSpecifiedDirective), so Apollo composition rejected the subgraph with Unknown directive "@oneOf". Reinstated on both federation seams from one source of truth: codegen-side OneOfDirectiveSdl (DEFINITION / usesOneOf / augment) wraps the file arm’s generateServiceSDLV2 output in SchemaSdlEmitter.printFederationServiceSdl; the generated <outputPackage>.util.OneOfDirectiveSdl (OneOfDirectiveSdlGenerator, wired into GraphQLRewriteGenerator under a federationLink && usesOneOf gate so a non-federation @oneOf schema emits no dead helper) corrects the runtime _Service.sdl by re-printing, appending the definition, and reinstalling a StaticDataFetcher, with GraphitronSchemaClassGenerator wrapping the federation build’s return in `withOneOfDefinition (inside if (federationLink)) under the usesOneOf gate. The one drift-prone string, DEFINITION, is single-sourced from the codegen constant; non-@oneOf schemas keep byte-identical output. @oneOf fixture (FilmOneOfFilter) added to the federated and shared sakila schemas. Coverage: SchemaSdlEmitterTest (federation-arm emit + no-op byte-stability guard + plain-arm regression guard), FederationBuildSmokeTest.serviceSdlExposesOneOfDirectiveDefinition (execution-tier { _service { sdl } } carries application + definition, no errors), OneOfDirectiveGateTest (federation emits the helper; non-federation does not while its plain SDL still carries the definition, pinning the federationLink conjunct). R253 cross-reference note added so its controlled-printer route preserves the @oneOf carve-out. Full reactor green.

  • R149 (609dc70): End-to-end producer-side test that GraphQLRewriteGenerator.buildOutput() populates both halves of BuildOutput.report(). New BuildOutputReportPipelineTest (@PipelineTier) drives a hand-written schema against the test jOOQ catalog carrying two independent diagnostics, an unresolvable @reference key (→ UnclassifiedField → report().errors()) and a redundant @record (→ build warning → report().warnings()), and asserts both halves are non-empty with content-specific matchers, closing the producer-side gap R147 deferred. The spec was narrowed to this one bullet: bullet 1 (end-to-end LSP publish-diagnostics) had already shipped under R196 as BuildTriggerPublishesDiagnosticsTest. NOTE: the R149 work landed under the mislabeled commit 609dc70 ("R266 rework: UK-delete execution proof…"), a rebase artifact that also carries the wrong session trailer; git show 609dc70 --stat shows only the three R149 files (the test, the narrowed spec, the README row). Full reactor green.

  • R285 (58a7c8f + d7d2861): Lift-back projection for child @service fields returning a table-bound type (ChildField.ServiceTableField). A ServiceTableField was emitted as a terminal record producer (buildServiceRowsMethod returning the service result verbatim), so non-column sub-fields on the returned type, the first being a @reference correlated multiset, failed at query time with Field "<name>" is not contained in row type. The fix routes ServiceTableField through the new SplitRowsMethodEmitter.buildServiceTableLift rows-method: call the @service method, extract each returned record’s PK, re-project the bound table on that PK by identity through Type.$fields(...), carry parentIdx + seq for scatter and intra-parent order, and re-wrap into the loader container (List/Map × list/single). The loader value type becomes the projected org.jooq.Record, not the developer-returned XRecord. This is the condensed ServiceRecordField -> RecordTableField shape from the spec’s fork resolution: no new sealed variant, no model change, the FK-hop-vs-identity distinction already lives on the source/key axis (SourceKey.Reader.ServiceTableRecord). All ServiceTableField`s now lift uniformly (scalar-only included), which preserves any service-applied filtering and avoids an emitter-side predicate branch. `TypeFetcherGenerator dispatch assembles the service call and sets the loader value type to org.jooq.Record. Validator: validateServiceTableField gains a return-table-PK guard (identity re-projection needs the returned table’s PK), mirroring the parent-PK check. Coverage: execution GraphQLQueryTest.films_castMembers_referenceSubfieldResolvesViaServiceTableFieldLift (mapped + list, Film.castMembers over FilmActor with actor: Actor @reference, asserting exact per-film cast so the reference resolves, no cross-parent leakage, no widening); pipeline both containers (FetcherPipelineTest positional loadFilms returns List<List<Record>> + serviceField_mappedContainer_rowsMethodReturnsMapOfProjectedRecord); unit ServiceFieldValidationTest.RETURN_TABLE_NO_PK; structural pins updated to the projected-Record loader value (TypeFetcherGeneratorTest, UnifiedEmissionPinsTest skeleton-route count 6 → 7). Drop-out semantics (returned key with no matching row falls out of the identity JOIN) are structurally guaranteed and the no-widening property is asserted, but not separately execution-tested since sakila film_actor rows are all real FK rows. Full reactor green.

  • R286 (12a9f88 + 77362d3): Allow @key(resolvable: false) on non-table-bound types (reference-only federation entity stubs). EntityResolutionBuilder.build()’s second loop (classified-type loop) now skips a non-table-bound type when every `@key directive on it is resolvable: false (no demote, no EntityResolution): it is a reference-only stub the subgraph declares for the supergraph composer but does not resolve, so it needs no backing table and emits no _entities handler. When at least one key is resolvable the R176 table-required diagnostic still fires. The decision turns on the federation resolvable flag alone, never on @record (or any) classification; a reachable stub then rides the ordinary classified-type path into the served _service.sdl carrying its @key(... resolvable: false). The R276 first-loop (absent-from-registry orphan) rejection is explicitly out of scope: an over-scoped first-loop relaxation shipped at 01696e1 was reverted at 77362d3 (orphans never reach the runtime SDL, which GraphitronSchemaClassGenerator.planFor builds from the registry, so the relaxation only suppressed the error without surfacing the type). Coverage: unit EntityResolutionBuilderTest.resolvableFalseKeyOnRecordType_isAcceptedAsReferenceOnlyStub + mixedResolvableAndNonResolvableKeysOnRecordType_stillRejects; execution-tier FederationBuildSmokeTest.serviceSdlExposesNonTableBoundResolvableFalseStub (new FilmRefStub fixture, a non-table-bound service-bound record carrier reachable via Query.filmRefStubs, asserts the type and its @key(... resolvable: false) reach the served { _service { sdl } }); resultEntityUnionContainsAllFixtureEntities updated to expect FilmRefStub in _Entity (federation-jvm includes every @key type regardless of resolvability, exactly like the table-bound Language stub; benign since resolvable: false governs composer routing, and FederationEntitiesDispatchTest stays green with the resolution-less member present). Follow-up R289 filed to correct the KeyNodeSynthesiser opt-out javadoc, which still claims resolvable: false keeps a type out of _Entity. Full reactor green.

  • R292 (4102697 + self-review 7a34e4e): Descriptions on synthesised Connection/Edge/PageInfo boilerplate. Graphitron-synthesised relay types carried no SDL descriptions, tripping Apollo’s ALL_ELEMENTS_REQUIRE_DESCRIPTION linter on every generated Connection/Edge type and field (20+ violations on a real consumer schema). The fix lives entirely in ConnectionPromoter, the single synthesis site: thirteen canonical graphql-relay-js wording constants with .description(...) added to the type builder and each field definition in buildSynthesised{Connection,Edge,PageInfo}. Single source of truth, parity for free: the description rides on the synthesised GraphQLObjectType carried by ConnectionType/EdgeType/PageInfoType.schemaType(), so it lands on both published seams (SchemaSdlEmitter’s `SchemaPrinter file output and ObjectTypeGenerator’s runtime rebuild, which reads `getDescription() per type/field) with no second emission site. Generic wording, not parameterised by element type. The structural (SDL-declared) Connection/Edge/PageInfo path is untouched: consumer-owned descriptions, and the SDL declaration is the override lever; the synthesis path only runs when the type is absent from the SDL. Coverage: unit ConnectionPromoterTest.directiveDrivenSynthesis_carriesRelayDescriptionsOnTypesAndFields (asserts descriptions on schemaType() and every field against the GraphQL model, including synthesised PageInfo whose fixture declares none); pipeline SchemaSdlEmissionTest.synthesisedConnectionBoilerplateCarriesRelayDescriptions (re-parses the emitted schema.graphqls structurally, confirming descriptions survive the SchemaPrinter seam); pipeline SynthesisedConnectionRuntimeDescriptionTest.runtimeRebuiltSchemaCarriesSynthesisedConnectionDescriptions (reads descriptions off Graphitron.buildSchema’s runtime `GraphQLObjectType, a genuine pin of the runtime seam that FederationBuildSmokeTest.emittedSdlMatchesRuntimeSchema’s `SchemaDiffing cannot see because it does not walk descriptions as graph vertices). The sakila fixture declares PageInfo structurally, so the pipeline tests scope to the genuinely-synthesised QueryStoresConnection/QueryStoresEdge; synthesised-PageInfo is pinned at the unit tier. Full reactor green.

  • R294 (36926c4 + aa3c772): Treat generator warnings in test fixtures as errors unless asserted. Establishes the policy that fixture builds treat generator warnings as errors unless the fixture’s point is to assert the warning path. Phase 1 (channel unification): the @asConnection same-table required-@nodeId hygiene advisory now rides ctx.addWarning(new BuildWarning(..., locationOf(fieldDef))) instead of the dedicated ASCONNECTION_HYGIENE_LOG SLF4J category, which is retired, so every classifier advisory surfaces on schema.warnings() as one inspectable record; AsConnectionSameTableWarnFormatTest migrated off logger capture onto schema.warnings(). Phase 2 (cleanup + gate): removed the 11 redundant @record directives and the redundant @splitQuery from the sakila-example schema (their warning paths stay owned on minimal SDL by R96RecordBindingPipelineTest and SingleRecordTableFieldServiceProducerPipelineTest), and added FixtureWarningsGateTest, which builds the example schema via buildOutput() and asserts schema.warnings() is exactly the one expected (message, coordinate) advisory: symmetric drift protection (a new accidental warning fails the size assertion, a vanished one fails the content assertion). Open check resolved: filmsConnectionByRequiredIds exists to prove R113’s production shape (required same-table @nodeId + @asConnection) ships a working WHERE-pk-IN connection, so the shape and its one warning stay; the floor is one expected advisory rather than zero. Out of scope and filed separately: the consumer-facing failOnWarning Mojo feature with typed WarningKind allowlist, deprecated-usage warnings (R296), and R293’s remaining non-generator warning categories. Split out of R293. Full reactor green.

  • R287 (6e3ef42): Remove the DELETE → @table return path. DELETE cannot legitimately project a full @table: the row is gone after the statement and RETURNING carries only the primary key, so the old path filled non-PK columns with null, fabricating an entity. The shape is now rejected at classification (author-facing) on two sites: MutationInputResolver.validateReturnType rejects a direct-return DELETE → @table (closing the MutationDeleteTableField @table path so it only ever holds an Encoded* arm), and FieldBuilder.classifyDeletePayloadField rejects a @table-element data field on a DELETE payload carrier, both naming why and pointing at the ID return. MutationDeleteTableField gains a compact constructor rejecting a Projected* DmlReturnExpression arm as a runtime backstop (the Projected* arms stay live for INSERT/UPDATE/UPSERT, whose rows survive the statement); the spec considered and declined narrowing the component type via a sub-sealed DmlReturnExpression on blast-radius grounds. The now-dead carrier and its support chain are deleted: ChildField.SingleRecordTableFieldFromReturning, PkResolution (whose NonPkNullable null-fill arm was the wrong behaviour itself), PerFieldOutcome, BuildContext.classifyDeleteTableProjection/DeleteTableProjection/classifyElementFieldForDeleteProjection, FieldClassification.SingleRecordTableFromReturning (+ CatalogBuilder/LSP arms), FetcherEmitter.buildSingleRecordTableFromReturningFetcherValue (+ dispatch), the TypeFetcherGenerator dispatch entry, and the GraphitronSchemaValidator no-op arm. SingleRecordIdFieldFromReturning (encoded-PK-off-RETURNING, deletion-safe) stays. Coverage: validation-tier MutationDeleteTableFieldValidationTest pins both author-facing rejections SDL-driven (no hand-built illegal field); the corpus retires the DELETE @table verdict, repointing DELETE roots to [Dml]/Column encoded-ID returns and dropping the DELETE payload examples (covered under the nodeidfixture, carried in VariantCoverageTest’s `NO_CASE_REQUIRED for MutationDeletePayloadField/MutationBulkDeletePayloadField); LeafTupleAdapter’s refusal arm and `PkResolutionEmitterReachabilityTest retire with the type; the sakila example drops deleteFilmsTableCarrier/DeletedFilmsTablePayload/DeletedFilmInfo and the execution proof, keeping deleteFilmsIdCarrier. User docs (code-generation-triggers.adoc), model/generator javadoc, and roadmap cross-references corrected so nothing presents DELETE → @table as designed. Discovered during R281 dimensional-model design and independently flagged by the 2026-06-10 staleness audit. Full reactor green.

  • R301 (3802964 + d0e8dde + d99b9ab): Align docs and javadoc with the R276 @record removal. @record is now parsed-but-ignored: the backing Java class is reflection-derived from the producing field (an @service return, or parameter for inputs; a @table resolution; a @tableMethod return; or a parent-accessor chain), and a reachable type still carrying it warns to remove it (redundant / shadowed-by-@table / disagrees-with-reflection, the three variants emitted at TypeBuilder.emitDirectiveIgnoredWarnings). The reference page record.adoc is rewritten as a deprecation/ignored page; deprecations.adoc + both directive indexes mark @record ignored (precedent: @index); directives.graphqls reframes the @record description as DEPRECATED/IGNORED and drops @record from the argMapping-inert list (matching checkArgMappingInert, which no longer fires for it); explanation pages, how-to guides, and the rewrite-internal adocs shift from @record-bound/-declared/-parent to reflection-derived class-backed / record-backed terminology with @record(record:) stripped from every worked example in favour of the producing field; javadoc across model/, catalog/, and generator classes reworded comment-only (the lone code change adds record to DeprecationsDocCoverageTest.WHOLE_DIRECTIVE_DEPRECATIONS). The generated supported-schema-shapes.adoc was regenerated from the updated leaf javadocs (also resyncing pre-existing drift: PlainObjectType removed per R276, newer leaves added); leaf-coverage --verify reports up to date. Runtime diagnostic strings (@record parent, a @record type) left untouched as out of scope. Full reactor green.

  • R312 (2524d8c): Thread CompositeDecodeHelperRegistry through the inline/split reference-field filter emitters, fixing a codegen crash when a filter input on a reference/list child field mixed @nodeId-decoded fields with @condition fields. Part A: own-and-drain a per-class decode registry at the two class-assembly points that host reference-field filter sites; TypeClassGenerator owns one registry per <Type> class (threaded through build$FieldsMethod → emitSelectionSwitch including the NestingField recursion into InlineTableFieldEmitter/InlineLookupTableFieldEmitter), TypeFetcherGenerator one per <Type>Fetchers class (threaded into SplitRowsMethodEmitter.buildFor* and buildQueryLookupRowsMethod). A new CompositeDecodeHelperRegistry.collectInto(TypeSpec.Builder, Consumer<…>) bracketing helper co-locates construct and drain so a lifted helper can never be silently dropped, with QueryConditionsGenerator refactored onto it so there is one drain implementation. Part B: guard the empty-join-path (standalone-lookup) shape uniformly with ParentCorrelation.checkCarrierInvariant ; InlineTableFieldEmitter emits a correlation-free conditions-only subquery (synthetic terminal alias + pre-switch DSL.noCondition() seed), InlineColumnReferenceFieldEmitter projects the column directly off the parent alias, and SplitRowsMethodEmitter throws a descriptive classifier-invariant error instead of an opaque Index -1. The ArgCallEmitter:372 null-registry throw is kept as the backstop. Coverage: NodeIdReferenceFilterPipelineTest (inline + split lift asserting the lifted private static decodeBar* helper, condition-only real-FK regression guard, empty-join-path standalone) plus a graphitron-sakila-example compilation-tier fixture (FilmMixedNodeIdConditionFilter on inline and @splitQuery reference fields) as the cross-module forgotten-drain backstop. No new STUBBED_VARIANTS entry; the PROJECTED_LEAVES "fully implemented" claim stays honest. Full reactor green under -Plocal-db.

  • R313 (adfaeff + build-through 43645d9): Fix @scalarType / convention scalars registering under the constant’s intrinsic name instead of the SDL name. A scalar whose SDL name aliases the constant it resolves to (the canonical case scalar LocalDate @scalarType(scalar: "graphql.scalars.ExtendedScalars.Date"), whose constant is named Date; and the convention sibling GraphQLBigDecimal, whose constant is named BigDecimal) was emitted as additionalType(<constant>), registering graphql-java under the constant’s name, so every typeRef(<sdlName>) bound to nothing and the generated GraphitronSchema.build() threw type <SdlName> not found in schema at runtime (surfaced downstream as Sikt’s SakMerknaderShapeTest build failures). The fix routes the mismatch through the existing ScalarResolution.Synthesised arm rather than teaching the emitter a new branch: ScalarTypeResolver.resolveFromConstantFqn gains an SDL-name-aware overload that forks on check.scalar().getName().equals(sdlName) (match → Resolved as before; mismatch → Synthesised(javaType, sdlName, owner, field), borrowing the constant’s coercing), with the SDL name threaded in via resolveFromDirectiveValue / resolveByConvention (built-ins never alias, so resolveBuiltIn is untouched); TypeBuilder’s `@scalarType and convention arms widen from instanceof Resolved to instanceof Successful, mirroring the federation arm. No new model component, no new emitter branch, no .javaType() reader change. Coverage spans all three tiers: resolver-tier ScalarTypeResolverTest (alias → Synthesised, match → Resolved, the GraphQLBigDecimal sibling, and the convention-loop widened to Successful); pipeline-tier GraphitronSchemaClassGeneratorTest (alias emits the scalar_LocalDate() synthesised helper, not the bare constant; Money no-regression keeps the plain additionalType(...MONEY) form); GraphitronSchemaBuilderTest.DIRECTIVE_BEATS_CONVENTION corrected to the Synthesised outcome (latently broken before); and the load-bearing execution / build-through added on review (43645d9): a graphitron-sakila-example fixture scalar LocalDate + Customer.createDate on the real customer.create_date DATE column, with graphql-java-extended-scalars at compile scope, whose GraphQLQueryTest.aliasingScalar_registeredUnderSdlNameAndResolvesEndToEnd asserts the assembled schema registers LocalDate (and Date does not leak) and projects ISO date strings end-to-end against PostgreSQL, reproducing the runtime failure on pre-fix code. Full reactor green under -Plocal-db.

  • R317 (slices 1–5, collapse c084745 + immutable-validate b0f4305/6930c25, In Review d347cca): Single edge-driven classification pass and immutable validation; TypeBuilder.buildTypes retired. R279 left the reachable surface traversed three times (a SchemaReachability name-set walk, buildTypes’ eager type loop, `buildSchema’s field loop) with the real verdict for directiveless objects scattered across three post-passes (promoteSingleRecordPayloads, `registerNestingTypes, the orphan arm of rejectDanglingTypeReferences). R317 collapses all three into one SchemaReachability.walk driving a real GraphQLTypeVisitor (GraphitronSchemaBuilder.ClassifyingVisitor) that classifies each composite on enter and folds its fields' classification into the same visit, governed by the read-free visitor invariant (the classifying visit may only register, never read the registry under construction). The three scattered post-passes fold onto the producing/embedding edge as registry-free verdicts (carrierTableBinding, isDirectivelessNestingTarget, the slice-3c edge orphan); the two reverse-lookups become pure typename-keyed fixed-point indices (ctx.tables/ctx.nodes/ctx.errors) threaded as traverser arguments; target-verdict reads at field edges go through a registry-free TypeBuilder.lookAheadVerdict (forced by graphql-java 25’s enter-only traversal, where a field’s output target is a not-yet-visited child). buildTypes, the reachableOutputTypes hand-off, and the field loop are deleted. Slice 5 inlines R318: the five global soundness reductions (node-typeId uniqueness, case-fold collisions, the dangling backstop, federation @key, multi-producer DomainReturnType agreement) now register a ValidationError on a single GraphitronSchema.diagnostics channel (via BuildContext.addDiagnostic) the validator drains, instead of demoting a settled verdict to UnclassifiedType/UnclassifiedField; a verdict read after the walk equals the verdict classification produced, and the ValidationError stream / which schemas pass or fail stay byte-identical. The NodeIndex is one-to-many by table (a table may back several @node types); implicit-encoder ambiguity moved to a use-site rejection with a disambiguation hint, correcting the old findFirst() arbitrary pick. The field-relative input model split to R327 (the one non-byte-identical change); R319 (warn-on-prune) stays separate. Coverage: the falsifiable acceptance test SingleWalkClassificationOrderTest (a deep target’s type-classify trace follows its discovering field’s, which an eager type pass fails) plus NodeIdPipelineTest (MULTIPLE_NODE_TYPES_PER_TABLE_ALLOWED, TYPE_ID_COLLISION_DEMOTES_BOTH), MutationDmlNodeIdClassificationTest.idReturnOnMultiNodeTable_ambiguous_rejected, GraphitronSchemaBuilderTest (SERVICE_MUTATION_ID_CARRIER_UNBOUND_ORPHAN_REJECTED_AT_EDGE, the orphan/case-fold cases now asserting the verdict stays real), EntityResolutionBuilderTest, and AppliedDirectiveEmitterTest (FEDERATION_SDL given a @table so User classifies). Truth table 448; folds in and discards R325 (read-free visitor restatement). Full reactor green under -Plocal-db (execution tier 413 tests).

  • R331 (f912d7f): Scope LSP @field(name:) validation/hover/completion on @table-interface participant cross-table reference fields to the @reference terminal table. FieldClassification.ParticipantCrossTable was the one column-bearing permit still in the FallThrough arm of lspColumnDispatch(), so the three column-name LSP consumers dispatched on the enclosing participant @table rather than the terminal table; a single-table-interface participant field reaching a column on another table via @reference drew a false-positive Unknown column … on table '<participant table>' squiggle on a schema that builds clean, plus wrong-table hover and completion. Fix is a single-arm relocation to Resolve(c.targetTableName()), the same routing R224/R233 gave the four other column-bearing permits; the record stays distinct for the FK-constant/alias hover surfaces (DeclarationHovers/InlayHints/LspClassificationLabels), which pattern-match it directly and are unaffected. Coverage mirrors the R233 trio with the interface-participant dimension: DiagnosticsTest (valid column → no diagnostic; bogus column → Unknown column 'NOPE' on table 'language', never citing the participant table 'film'), HoversTest, and FieldCompletionsTest. Full reactor green under -Plocal-db.

  • R330 (8197af1 + 84102d6 + 06b47de + e9d80fc): Fix @condition(override: true) on a @nodeId FK-target filter field passing the parent’s root table instead of the joined FK-target alias, a v9→v10 parity gap surfacing as incompatible types at consumer compile (e.g. a Soknadsmangeltype handed to iRegelverksamling(Regelverksamling, ...)). An FK-target @nodeId field’s developer @condition method expects the FK-target table X reached through a foreign-key join path, not the input’s own table, but the rewrite’s no-join liftedSourceColumns model never propagated the join into the @condition method’s ParamSource.Table slot, so the emitter passed the literal "table" for every condition method. The model gap is lifted into a sealed WhereFilter sibling FkTargetConditionFilter (alongside ConditionFilter/GeneratedConditionFilter) carrying the target TableRef, the resolved FkJoin joinPath, the lifted FK-child source columns, and X’s key columns; `FieldBuilder.walkInputFieldConditions wraps both the single-column ColumnReferenceField and composite CompositeColumnReferenceField arms in it whenever the join path is non-empty. Every WHERE-emitting site forks on the type through a shared FkTargetConditionEmitter (declareAliases + emitTerm) so the FK-target arm is defined once and cannot drift across the five sites (QueryConditionsGenerator shim, InlineTableFieldEmitter, InlineLookupTableFieldEmitter, SplitRowsMethodEmitter, TypeFetcherGenerator.buildQueryLookupRowsMethod); the plain arm stays byte-identical. The FK-target arm emits a correlated DSL.exists(DSL.selectOne().from(X).where(<correlation>.and(method(X, args)))) (approach B over restoring the legacy top-level join, keeping the (Table, env) -> Condition shim contract and staying inside the JoinStep cardinality invariant); the correlation reuses JoinPathEmitter.emitCorrelationWhere, which ANDs every FK slot, so composite-key FK targets work for free with no RowN. Recursing inline/lookup/split sites runtime-prefix their SQL aliases onto the base alias’s getName(); the two top-level method sites use static aliases. Both reported instances fixed (SoknadsmangeltypeFilterInput.regelverksamlingId shim path in pass 1; EndringsloggV2FilterInput.brukerId inline child path in the rework). Composite-key NodeType targets are now supported rather than deferred to R24 (they are the common consumer shape); the validator’s composite rejection narrowed from a blanket deferral to the same non-FkJoin-hop guard the single-column case uses, mirroring the emitter precondition. The per-argument nested-ternary extraction readability work was split out to R334. Coverage: pipeline NodeIdOverrideConditionFkTargetPipelineTest (single-column + composite FK-target carrier assertions, no code-string assertions on method bodies); sakila compile-tier guards (concrete Address/Project condition parameters) and execution assertions for shim, shim+field-override, inline child, @splitQuery child, multi-field shim, composite @table, and plain-input composite on both list and @asConnection. Full reactor green under -Plocal-db.

  • R338 (f40b056 + warnings-gate 371a5eb): Split-query correlation now keys both cardinalities off the FK’s referenced columns instead of the parent PK. A list @splitQuery reference field whose @reference FK targets a non-PK unique key on the parent silently returned zero rows for every parent: FieldBuilder.deriveSplitQuerySource built the parentInput VALUES table from the parent’s PK columns on the List (child-holds-FK) branch, while SplitRowsMethodEmitter’s correlation predicate references the FK’s actual referenced columns (sourceSideColumns()); when those are not the PK, `parentInput.field(...) resolved to null and the predicate degraded to col = NULL, matching nothing with no error raised. The fix drops the !isList guard so both cardinalities key off the first hop’s sourceSideColumns() when the first hop is an FkJoin (BuildContext.resolveFkSlots already orients a child-holds-FK first hop so the slot’s source side is the parent’s referenced columns), keeping the primaryKeyColumns() fallback only for the non-FK first-hop (ConditionJoin) shape, where ParentCorrelation.OnConditionJoin correlates on parent PK. The read-side machinery already reads arbitrary FK source columns off the parent record (the Single branch and deriveFkRecordParentSource prove this), so no emitter change was needed; the stale parent-PK-assumption javadoc was rewritten. Coverage: execution-tier GraphQLQueryTest.splitTableField_fkReferencesNonPkUniqueKey_returnsChildRows over a new split_parent (PK parent_id, UNIQUE parent_code) + split_parent_tag (FK → split_parent.parent_code) fixture, asserting the child rows scatter per parent by the unique-key value (ALPHA two tags, BETA one) and the batch fan-in stays at two round-trips, behavior-asserted with no code-string assertions; verified to fail (empty list) with the fix reverted. FixtureWarningsGateTest’s pinned schema line updated for the added Query field. Full reactor green under `-Plocal-db.

  • R339 (cc45dbb): Honour @defaultOrder directive-level direction: on the primaryKey: and index: variants. OrderByResolver.resolveOrderEntries already threaded the resolved defaultDirection into the fields: branch but hardcoded SortDirection.ASC on the two sibling branches, so @defaultOrder(primaryKey: true, direction: DESC) (and the index: variant) silently sorted ASC, violating the directive’s published contract (direction: SortDirection = ASC declared directive-level with no per-source carve-out). This reverses R243’s "fork (b)" for @defaultOrder only: the primaryKey: branch now stamps defaultDirection onto each synthesised PK entry, and resolveIndexColumns takes a SortDirection parameter that the @defaultOrder call site feeds defaultDirection while the @order enum-value alias still passes ASC (its direction comes from the runtime input object’s direction: field, flipped in the *OrderBy helper at code-generation time, not the directive). The directive-absent implicit-PK fallback in resolveDefaultOrderSpec stays ASC. No emitter or seek change was needed: uniformAsc, emission (jooqMethodName()), and keyset seek already derive from per-column direction, so an all-DESC PK/index default yields uniformAsc == false and paginates descending end to end automatically. Coverage: pipeline-tier GraphitronSchemaBuilderTest rewrote DEFAULT_ORDER_DIRECTION_DESC in place (no stale fixture left alongside) to assert uniformAsc() == false / direction() == DESC, and added DEFAULT_ORDER_INDEX_DESC; execution-tier GraphQLQueryTest.filmsConnectionDesc_executesDescendingPrimaryKeyOrder over a new filmsConnectionDesc connection asserts filmId order 5..1 (the exact reverse of the PK-ASC baseline), proving emitted .desc() + descending keyset seek with no code-string assertions; FixtureWarningsGateTest’s pinned schema line updated for the added field. Full reactor green under `-Plocal-db.

  • R90 (fa07632 Phase 1+2 + 2dd6fbb Phase 3 + e066346 Phase 4 + docs 6d3ac86): LSP Java-source surfacing for goto-definition, Javadoc hover, @externalField completion, and argMapping. Framed as expanding the catalog data CatalogBuilder.build already exports to the LSP, not a new feature: the SourceLocation / description slots existed but the jOOQ half exported only file-level (0:0) positions and the service half (ExternalReference / Method, bytecode-only) carried no source location or Javadoc at all. New SourceWalker (parse-only JDK Compiler Tree API, no external dependency; per-file mtime cache so a .class-only watcher trigger re-parses nothing) recovers declaration positions and Javadoc from the consumer’s compile source roots, threaded through RewriteContext.compileSourceRoots (populated from MavenProject.getCompileSourceRoots() in AbstractRewriteMojo). CatalogBuilder is the single join site: it rebuilds the immutable Table / Column / ExternalReference / Method records from the ClasspathScanner structure plus the walk index in one pass (jOOQ half refined to per-line + field Javadoc; service half gains a SourceLocation component with UNKNOWN-defaulting back-compat factories), keying methods on (className, methodName, paramCount) and dropping overload-ambiguous keys to UNKNOWN rather than binding a wrong line. Definitions.compute gains a service-half arm reusing LspVocabulary.behaviorAt / siblingStringAt for @service / @externalField / @enum / @condition / @sourceRow / @tableMethod (@record carved out, mirroring completion/hover); Hovers renders class/method Javadoc; ExternalFieldCompletions narrows the method list to single-parameter Field-returning lifters; ArgMapping (pure string-content decomposition) + ArgMappingCompletions + argMapping diagnostics cover the javaParam: graphqlArg grammar (left = method parameter names, right = enclosing field’s GraphQL args, head-segment only for R84 dot-paths). Two documented in-scope approximations bounded by the spec’s out-of-scope list: the @externalField Table-parameter check uses the catalog-derivable signature shape (the classifier-driven Parameter.source = ParamSource.Table projection is generator-side work the LSP catalog does not carry), and argMapping dot-paths validate/complete the head segment only (the snapshot carries no nested input-field projection for arbitrary input types). Coverage: LSP-tier DefinitionsTest (one case per binding directive + @record carve-out + unknown-name / overload-UNKNOWN fall-throughs), pipeline-tier CatalogBuilderSourceTest (column + service-half refinement and Javadoc lift from a synthetic source root; UNKNOWN fallback when roots absent), unit-tier SourceWalkerTest (overload-ambiguity, doc-comment retention, unparseable-file tolerance, mtime cache invalidation, no params/locals as fields), plus ArgMappingTest / ArgMappingDiagnosticsTest / ArgMappingCompletionsTest / ExternalFieldCompletionsTest / HoversTest; no code-string assertions (no generated output, positions / Location`s are the asserted shape). `getting-started.adoc documents the editor surface. Predecessor R18. Full reactor green under -Plocal-db.

  • R343 (72440a0a0): LSP column-name completion / hover / validation for @defaultOrder(fields: [{name: ...}]). Binds the FieldSort.name coordinate to Behavior.CatalogColumnBinding in the LspVocabulary canonical overlay; previously the site fell through to ArgNameCompletions and offered no column suggestions, nudging authors to hand-write an ordering condition resolver instead of the declarative @defaultOrder. The crux was which table’s columns to offer: a list/connection field’s ordering columns live on the navigated (element) table, not the enclosing type’s @table. Resolved in the classification rather than the LSP, so all three surfaces agree on the terminal table, by relocating TableTarget / RecordTableTarget from the FallThrough arm of FieldClassification.lspColumnDispatch() to Resolve(tableName()) (the element table), the same single-arm pattern R331 gave ParticipantCrossTable; the @reference-backed shape already resolved via ParticipantCrossTable. No new completion provider. Coverage: pipeline-tier LspColumnDispatchProjectionTest (plain list, @asConnection @splitQuery connection, and @splitQuery shapes all classify TableTarget and dispatch Resolve(element-table)), FieldCompletionsTest (element-table columns offered not the enclosing type’s, across plain / connection / @reference`@splitQuery+; negative for the `primaryKey: site), HoversTest + DiagnosticsTest (hover and column validation cite the element table; a bogus column reported on it), and DriftDetectionTest (FieldSort.name resolves and binds to the column behavior under the startup invariant); no code-string assertions. Builds on R119 and R233. Full reactor green under -Plocal-db.

  • R356 (27fc6d4, Spec 5e9ae41): Unify the per-column shared-column overlap analysis across the six accreted DML mutation write-path sites onto one shared primitive. The "group writers by backing column, keep size-two-or-more, an all-plain overlap is a build-time reject and a decode-involving one needs a runtime value-agreement check" grouping was hand-rolled in six places (R322/R354/R342). New model/ColumnOverlap introduces a minimal read-only ColumnWriter view (targetColumns() in decode-record slot order, decode(), label()), a Contributor, an OverlapColumn (shared() / allPlain()), and groupByColumn ; a pure structural fold over already-resolved sqlName values invoked once per site, not a model-carried fact (the @mutation validator runs at resolution time, before the emit carriers exist, so a per-carrier stored fact would force the validator to keep its own walk). JooqRecordInstantiationEmitter (site 1) retires analyzeOverlap + SlotRef, adapting Writer into the view; TypeFetcherGenerator’s `insertColumnPlan (site 2) and setColumnPlan (site 6) delegate to groupByColumn, retiring InsertCol/InsertColWriter and R342’s clone SetCol/SetColWriter onto OverlapColumn/Contributor, emitSetAgreementPreamble (site 4) replaces its inline byColumn map / int[] tuples, sites 2 and 4 route their value-read through the shared appendAgreementValue / emitAgreementDecodeLocal seam (already serving sites 5 and 6), and emitKeySetAgreementPreamble (site 5) adopts the SetGroupWriter leaf view + typed records while keeping its bespoke cross-partition intersection; MutationInputResolver (site 3) reads shared() && allPlain() off the same fold, making validator-mirrors-classifier structural. Each emitter downcasts Contributor.writer() back to its site view to reach the wrapped carrier. The doubly-stale R342 comment is corrected. Pure refactor: directive, model-carrier, wire-format, the shared requireColumnAgreement predicate, dispatch partitions and the Rejection taxonomy are untouched; site 1’s optional value-read fold and the outer gather-and-pairwise loop stay out of scope as specified. New unit-tier ColumnOverlapTest pins the grouping (encounter order, every column kept, shared() / allPlain(), the slot-ordering invariant); the inherited execution + pipeline net (NodeIdValueAgreementExecutionTest, SelfFkNodeId{Insert,Update}ExecutionTest, the R342 bulk cases, MutationDmlNodeIdClassificationTest, JooqRecordServiceParamPipelineTest, UpdateRowsWalkerTest, RejectionSeverityCoverageTest) stays green with no assertion edits. Builds on R322/R354/R342/R328. Full reactor green under -Plocal-db.

  • R353 (951aed0): LSP goto-definition from an SDL declaration name (a type name or a field / input-value name, not a directive argument) to the Java the model bound it to, the navigation handle the cursor naturally rests on and the only handle reflection-bound types carry. New definition/DeclarationDefinitions dispatches on the enclosing type’s TypeBackingShape via an exhaustive switch with no default (mirroring Definitions over Behavior, so a future backing permit forces a goto-def decision at compile time), resolving every arm through the sealed DefinitionTarget and the LSP-owned SourceWalker.Index R349 established: a type name jumps to the backing class (jOOQ table class for table-bound types; the consumer class for reflection-bound record / POJO / standalone-jOOQ types), and a field name jumps member-precise to the backing member (a jOOQ column, a record component indexed as a field by the parse-only walk, or a POJO bean accessor), with a field on a standalone jOOQ record degrading to its backing class and the @field(name:) override read off the field node to name the bound member. The shared declaration-name trigger is factored out of DeclarationHovers into parsing/SdlDeclaration (sealed TypeName / FieldName) so the hover and goto-def triggers cannot drift; DeclarationHovers.findContaining becomes a thin adapter over it. Corrected the signed-off D1’s inverted record/POJO premise: resolution is by source-index key, and MemberSlot.name() is the bean property name a POJO method index is not keyed by, so the slot is widened to carry the arity-0 accessorMethodName, populated at the one CatalogBuilder projection site (projectPojo passes method.name(), projectRecord passes rc.name()) so the bean rule keeps its single home and both axes are member-precise; this retires the deferred "member-precise record components" follow-up. Definitions.fieldTarget / resolve widened to package-private for the sibling provider; chained into GraphitronTextDocumentService.definition via a third .or(). Coverage: pipeline-tier DeclarationDefinitionsTest (one case per backing shape per axis: table-class / record / POJO / standalone-jOOQ type names, the SourceAbsent → empty degrade, table column, @field(name:)-overridden column, POJO accessor method, record component, standalone-jOOQ field degrade, unknown member, NoBacking, directive-argument non-trigger, unavailable snapshot), MemberSlot call sites updated across FieldCompletionsTest / HoversTest / DiagnosticsTest; no code-string assertions (resolved Location`s are the asserted shape). Builds on R349 and R90. Full reactor green under `-Plocal-db.

  • R366 (d71545d, In Review 143a155, Spec b6a6f93/88e61592): Emit loadMany dispatch for list-cardinality polymorphic @splitQuery on record-backed parents. MultiTablePolymorphicEmitter.buildBatchedListFetcher unconditionally emitted return loader.load(key, env), but for an AccessorCall/MANY (or ProducedRecordRead/MANY) parent SourceKey, GeneratorUtils.buildRecordParentKeyExtraction declares a loop-local List<…> keys rather than a single method-scoped key, so the generated fetcher referenced an out-of-scope local and failed javac (cannot find symbol: variable key) on a field that passed graphitron:validate. This is the polymorphic sibling of the already-fixed non-polymorphic wrapper split-query compile bug. Fix forks the load site on parentSourceKey.cardinality(), mirroring how TypeFetcherGenerator.buildRecordBasedDataFetcher branches load vs loadMany: ONE keeps loader.load(key, env); MANY emits loader.loadMany(keys, Collections.nCopies(keys.size(), env)) then concats the one-bucket-per-element List<List<Record>> into the field’s flat List<Record> via flatMap before the async tail (flatten matches the flat [Type!]! surface; the per-element grouping the SDL doesn’t ask for is deliberately not preserved). No model change (cardinality is on the SourceKey the fetcher already holds) and no new floor-guarantee rejection: AccessorCall/ONE is unreachable on a list field (FieldBuilder.collectAccessorMatches rejects a single-record accessor as CardinalityMismatch) and single-cardinality Pojo is already deferred-rejected, so the two live list-arm paths are ColumnRead/ONE (table parent) and AccessorCall/MANY (record parent), both now compiling. Coverage: compilation-tier fixture OccupantsBatchPayload (free-form @record exposing List<AddressRecord> addresses(), child occupants: [AddressOccupant!]! @field(name: "addresses") over the Customer | Staff union) + OccupantsBatchPayloadService + Query.occupantsBatch, whose generated OccupantsBatchPayloadFetchers.occupants emits the loadMany`+flatMap` dispatch and fails javac on any regression to load(key); the pipeline tier already classifies the AccessorCall/MANY shape. Sibling of R367 (single-cardinality guard); shares MultiTablePolymorphicEmitter with R363. Full reactor green under -Plocal-db (:graphitron 2216 tests, :graphitron-sakila-example 455 tests).

  • R364 (172016a, In Review 9eff0e4): Fix the @service @splitQuery rows-method return type for enum and non-built-in scalar leaf fields. A non-root @service child field whose GraphQL type is an enum (or any scalar outside the five GraphQL spec built-ins) generated a doubly-nested Map<KeyRecord, Map<KeyRecord, V>> rows method instead of the flat Map<KeyRecord, V>, so the generated code did not compile; sibling Int / Boolean fields were already flat. Root cause: ServiceRecordField.elementType() fell back to the service method’s whole Map<K, V> when RowsMethodShape.strictPerKeyType returned null (which it does for any name ScalarTypeResolver.builtInJavaType can’t resolve), and outerRowsReturnType then wrapped that map once more. Per the spec’s recommended option (a), the fix adds RowsMethodShape.perKeyFromOuter ; the structural inverse of outerRowsReturnType that peels the per-key V back out of a known outer Map<K, V> / List<V> across the (isMapped, isList) cross-product, returning null for an unpeelable shape ; and routes elementType() through it for the non-built-in scalar leaf (other null-perKey cases keep the legacy whole-type fallback); both call sites derive isMapped from sourced.container() (stored verbatim into LoaderRegistration at FieldBuilder.buildServiceLoaderRegistration) so emitter and validator cannot disagree. Spec step 2 closes the validator gap: ServiceDirectiveResolver.validateChildServiceReturnType no longer skips the non-built-in scalar case, instead peeling the leaf, reconstructing the expected outer shape, and rejecting a wrong key type / missing list-nesting / unpeelable container at classify time rather than leaving it to miscompile (a self-consistent leaf peel, honestly documented as a key-type-plus-container check, not full strict-equality). The deferred typing-fidelity follow-up (emit-text-mapped-enum-fields-as-enum-type) stays out of scope: the leaf is accepted as whatever the method yields. Coverage: unit-tier RowsMethodShapeTest (forward/inverse round-trip across the full (isMapped, isList) cross-product plus three null-rejection cases), pipeline-tier FetcherPipelineTest (enum-leaf mapped field emits the flat Map<Row1<Integer>, String> with the Int sibling unchanged; wrong-container field rejected at classify time), and TestFilmService fixtures; no code-string assertions on generated method bodies. graphitron and graphitron-sakila-example tiers green under -Plocal-db (execution tier 455 tests); graphitron-lsp not exercised at review (native libtree-sitter unavailable in the review sandbox, a known environment gap).

  • R371 (1327de0 + self-review 259ff2d): Declaration-name hover now overlays the bound jOOQ class / column / member Javadoc beneath the classification block, closing the asymmetry R369 exposed (goto-definition jumped into the jOOQ source on an SDL type-name / field-name token, but hover on the same token stayed classification-only and never read the source index). The fix makes hover/goto parity structural rather than asserted: a new parsing/DeclTarget sealed family (CatalogTable / CatalogColumn / SourceClass / SourceMethod / SourceField / None) is the one backing-switch from an SDL declaration coordinate to a named jOOQ / Java declaration, and the two consumers each project it exhaustively ; DeclarationDefinitions.locate to a Location (goto), DeclarationHovers.overlay to a Javadoc string (hover) ; so they cannot point at different declarations and a new TypeBackingShape permit breaks both switches at compile time. This collapsed the request-time backing-switch from three hand-rolled copies to two (the directive-value @field(name:) arm Hovers.columnHover still runs its own switch and still diverges on the F1/F3 cases; retiring it rides on the candidate follow-up that lifts DeclTarget onto Built). Standalone jOOQ records overlay their class Javadoc where goto jumps (spec F1), POJO accessors overlay the arity-0 method Javadoc and record components the component field Javadoc (F3), and only NoBacking.* yields neither; the table / column arms keep Descriptions’s SQL-comment-wins precedence. Goto behaviour is unchanged: `locate reproduces the prior per-arm Definitions.resolve calls exactly. Two spec-stated deviations, both justified: Descriptions.classJavadoc was promoted public rather than package-private (hover is a sub-package, so package-private would not reach it; the access shape F2 itself offered first), and locate / overlay are public test seams so the parity property is assertable without a tree-sitter round-trip. Coverage: tree-sitter-free unit-tier DeclarationHoverOverlayParityTest asserts the resolver per backing, the overlay text per variant, and the overlay-presence ⟺ jump-presence drift guard per DeclTarget variant (F4); the live DeclarationHoversTest stays classification-only via the back-compat 3-arg entry. No code-string assertions on generated bodies. Reviewed independent-session; DeclarationHoverOverlayParityTest green (5/5) and the graphitron-lsp reactor (-am) fully test-compiled under -Plocal-db; the live tree-sitter LSP tier was not exercised because the native libtree-sitter runtime is egress-blocked in the review sandbox (a known environment gap, the same one the implementer documented), and the diff’s live-path change is a minimal one-call delegation through the back-compat seam. Builds on R369 / R353 / R352 / R90 / R160.

  • R368 (938bb69): MCP structured read-tools over the live Workspace, landing R118 slices 3-6 as thin reads on the R361 seam. Five tools plus one resource registered the way statusTool is: services / conditions / records over Workspace.catalog().externalReferences() joined with Workspace.sourceIndex(); schema over Workspace.snapshot() joined same-cadence with @node metadata off catalog().nodeMetadata() (exhaustive switches over the TypeClassification / TypeBackingShape / FieldClassification / LspSchemaSnapshot permits, no default); diagnostics over Workspace.validationReport() reporting snapshot availability/freshness alongside; and a directives MCP resource (the new resources capability) composing the frozen bundled grammar with the live snapshot’s user-declared directives. Two owned, additive shared-model widenings, both via back-compat constructors so existing LSP/test callers compile unchanged: CompletionData.Method gains a typed returnsCondition fact classified at the parse boundary in ClasspathScanner from the un-erased return descriptor (exact Lorg/jooq/Condition; compare, so a consumer’s own type named Condition is not mis-tagged), and DirectiveShape gains an applicable-locations field projected at CatalogBuilder.buildSnapshot from DirectiveDefinition.getDirectiveLocations(). Shared wire mechanics (lenient arg coercion, opaque base64 page cursors, the fqcn#method/arity stable-ID grammar slice 7 will walk, and the typed SourceJoin left join ; sealed Resolved / NotIndexed / Ambiguous, never a silent drop or hard failure) factored into McpWire; the R362 catalog tools route through it too. No new generator branch and no validate-time arm: these are descriptive discovery reads, so validator-mirrors-classifier does not apply (re-derived in the spec given the scanner lift). Coverage: unit-tier ClasspathScannerTest (the parse-boundary condition classification incl. the false-positive guard), CatalogBuilderSnapshotTest (directive-locations round-trip + back-compat empty default), and MCP-handler tier in GraphitronMcpServerTest (services/conditions/records structured shapes with method refs and resolved/not-indexed location arms, the @node catalog join, schema paging + unavailable-before-build, diagnostics mapping + severity filter + snapshot-freshness, the directives resource listing bundled + user-declared with rendered locations, and the stable-ID/join-key round-trip) ; structured-content assertions, no code-string assertions on generated bodies. Builds on R361; sibling of R362. Independent-session In Review → Done review; full reactor green under -Plocal-db (ClasspathScannerTest 14, CatalogBuilderSnapshotTest 15, GraphitronMcpServerTest 21).

  • R148 (7fc5f5e): Re-anchor LSP validator diagnostics off the doc block onto the definition name. graphql-java anchors a described definition’s getSourceLocation() at the opening delimiter of its documentation block (the description is the AST node’s first token), so an R147 validator error on a documented type/field underlined the doc block rather than the declaration in the editor squiggle. The originally-planned BuildContext.locationOf line-arithmetic heuristic over description.getContent() was abandoned as unworkable: graphql-java’s processed content cannot distinguish an inline """text""" block (name on the next line, advance 1) from an own-line block (advance +3) ; both report `+multiLine=true` with zero interior newlines ; and inline blocks are the dominant style in directives.graphqls; BuildContext also has no raw SDL to scan. Fixed in the LSP, which holds the raw source and a tree-sitter parse: Diagnostics.signatureRange / descriptionNameRange resolve the validator SourceLocation to a tree-sitter point, and when it lands inside a description node re-anchor the diagnostic range to the enclosing definition’s name (or enum_value for enum-value definitions); otherwise the prior column-to-end-of-line fallback is preserved. Exact for every documentation form (single-line, inline block, multi-line block) and location-source-agnostic, so every validator error/warning routed through validatorDiagnostic is re-anchored with no BuildContext / GraphitronSchemaValidator call-site changes. Adds DESCRIPTION to GraphqlNodeKind. The build-time console / watch-mode formatter (graphitron-core) stays on the graphql-java location (no tree-sitter or raw source there) as a documented lower-priority follow-up. Coverage: ValidatorDiagnosticsTest one test per documentation form (own-line block on a type, inline block on a field, single-line on a type) asserting the range covers the name token, plus a no-description pass-through asserting the column-to-end-of-line fallback; no code-string assertions (diagnostic ranges are the asserted shape). Builds on R147. Independent-session In Review → Done review; full reactor green under -Plocal-db (ValidatorDiagnosticsTest 17 tests).

  • R388 (b6b629d): Fix two runtime defects in the discriminated-interface (@table + @discriminate) fetcher when a participant’s FK-target detail table re-declares the discriminator column via a composite FK. Defect 1 (TypeFetcherGenerator): the discriminator column was emitted as a bare DSL.field(DSL.name(col)) at all three sites (SELECT projection, LEFT JOIN ON-clause, WHERE filter), making the reference ambiguous and PostgreSQL reject the query once a participant join fired; now qualified to the base table via a two-part DSL.name(baseTableSqlName, col) (renders "base"."col" and preserves the Field<Object> the .eq(String)/.in(String...) predicates need, where a table-instance reference would type as Field<?> and fail to compile), with the base table’s SQL name threaded through buildInterfaceFieldsList / buildCrossTableJoinChain / buildDiscriminatorFilter. Defect 2 (TypeBuilder.extractCrossTableFields): a participant @reference field whose resolved column already exists on the interface/base table is a contradiction (the column is read directly off the discriminated base table, so a cross-table @reference is meaningless and the emitted fetcher reads a join-only alias never populated in a non-inline-fragment query); detected once with the catalog in scope, the field is skipped from the cross-table set and a build diagnostic is registered, surfaced through the validator’s existing drainBuildDiagnostics (the R204/R279/R317 pattern) as an INVALID_SCHEMA author error with file:line and a detail-column candidate hint. The spec called for validator-side emission in validateTableInterfaceType, but that method has no catalog access; the diagnostic-drain is the faithful realisation of the spec’s "resolve once, validator reads rather than recomputes" intent. A participant-only @reference field stays valid. Fixture: jti_subject + jti_app_account + jti_person joined-inheritance tables in init.sql (detail tables re-declare the discriminator via composite FK), corrected-shape Subject/AppAccount/Person SDL in the example schema, execution-tier regression tests (GraphQLQueryTest.allSubjects_returnsDiscriminatorPerRow + allSubjects_inlineFragmentDetail_joinsWithoutAmbiguousColumn with an SQL_LOG qualified-reference assertion), and validation pipeline tests (DiscriminatorReferenceContradictionPipelineTest, both the rejection and the participant-only positive case); no code-string assertions on generated method bodies. First-class discriminated joined-table inheritance (a participant declaring its own detail @table) remains out of scope as R389. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R380 (745c0cd): Emit a correlated EXISTS for an @reference(path:) filter whose terminal column lives on a joined table, on both filter surfaces. Previously the join path was carried but dropped at projection (input-object filter: fields, the motivating utdanningsregisteret bug: STATUS_SELVAKKREDITERENDE on LARESTED bound against ORGANISASJON, so the generated *Conditions.java did not compile) or never read at all (direct scalar ARGUMENT_DEFINITION, where the column resolved against the field’s own table from the outset). Both surfaces share one model + emitter spine and differ only in the classifier locus. Design A (chosen over lifting to a call-site WhereFilter): the EXISTS is emitted inside the generated <Type>Conditions.<field>Condition method, so every call site (QueryConditionsGenerator, InlineTableFieldEmitter) stays unchanged and the correlation ties back to whatever alias the caller passes. Model: a new sealed BodyParam.RemoteColumnPredicate(joinPath, inner) wraps an ordinary ColumnPredicate (columns bound to the terminal table) rather than bolting a joinPath onto the four operator/value-arity arms, mirroring how FkTargetConditionFilter wraps a ConditionFilter; name()/list()/nonNull()/extraction() delegate to inner. Emitter (TypeConditionsGenerator): the four local arms collapse into one ColumnPredicate arm via emitColumnPredicateTerm(cp, alias) + appendGuardedAnd (generated output for local predicates is byte-for-byte unchanged); the remote arm declares one method-local hop alias per FK step, builds DSL.exists(selectOne().from(terminal).join(...).where(<step-0 correlation back to table>.and(<inner on terminal>))), and the same null / empty-list guard wraps the whole EXISTS. Classifier (FieldBuilder): Surface 1 stops dropping the parsed path and wraps via remoteIfReferenceJoin, discriminating plain @reference (Direct extraction, terminal column → wrap) from the @nodeId FK-target lift (NodeIdDecodeKeys extraction, local FK-child columns → stay local); Surface 2 reads @reference before the local findColumn, parses the path, resolves the column against the terminal table, and carries it on ScalarArg.ColumnArg.joinPath. Validator mirrors the FK-only precondition at both sites. v1 deferrals (recorded in the implementation commit per the spec): ConditionJoin ({condition:}) hops rejected with a typed diagnostic, matching the output-side stub; composite terminals supported by the emitter (unit-tested) but not yet classifier-reachable; @splitQuery rides the same path with no new work. Coverage: pipeline-tier ReferenceFilterRemoteColumnPipelineTest (both surfaces lower to RemoteColumnPredicate; single-hop {table:}/{key:}, multi-hop, list In; nodeId-stays-local discrimination guard; ConditionJoin rejection ; model-level assertions) and execution-tier GraphQLQueryTest (single-hop scalar, two-hop scalar, the input-object filter field reproducing the motivating bug, and absent-arg-returns-all, asserting real rows against the seeded DB). join-with-references.adoc rewritten to state the correlated-EXISTS behavior, multi-hop, null/empty-list semantics, and the FK-hops-only limitation. Independent-session In Review → Done review; full reactor green under -Plocal-db. Carried debt: the new unit-tier TypeConditionsGeneratorTest cases pin the EXISTS body with code().toString() contains(...) assertions, matching that file’s pre-existing convention (R375/R79/R50) but contrary to the project’s "no code-string assertions on generated method bodies" principle; the behavior is independently proven at the execution + compilation tiers, so the code-string cases are redundant. Whole-file migration of TypeConditionsGeneratorTest off code-string assertions filed as a follow-up Backlog item. Builds on R379; siblings R236 / R282 / R330 and the deferred nodeid-fk-target-arg-join-translation.

  • R391 (c59a11c, Spec 4cfb5dc): Add a default-case Graphitron.newGraphQL() factory to the generated facade. GraphitronFacadeGenerator emits public static GraphQL.Builder newGraphQL() with body GraphQL.newGraphQL(buildSchema(customizer -> {})), so a zero-extra-wiring consumer writes Graphitron.newGraphQL().build() instead of GraphQL.newGraphQL(Graphitron.buildSchema(b -> {})).build(). Returns a builder (not a built engine), mirroring the newExecutionInput(...) convention so instrumentation / execution-strategy configuration stays open without a second overload; the body delegates to the facade’s own single-arg buildSchema, keeping buildSchema the single schema producer, and in a federation-linked build that path already returns the Federation.transform-wrapped schema, so no federation-specific overload is needed. Call-site sweep: the GraphqlEngine consumer exemplar plus 23 default-case execution-tier sites converted to Graphitron.newGraphQL().build() (enumerated in the landing commit); the two-arg buildSchema(b -> {}, fed -> {}) federation sites, the fetchEntities customiser, and the raw-SDL spike were deliberately left as-is. Coverage: unit-tier GraphitronFacadeGeneratorTest (structural only, no body-string assertion: method-list now expects newGraphQL, plus newGraphQL_isPublicStaticReturningGraphQLBuilder and newGraphQL_isPresentExactlyOnceInFederationBuild) and execution-tier FederationBuildSmokeTest.newGraphQLBuildsFederationWrappedEngine (builds via newGraphQL().build(), asserts _service { sdl } resolves with no errors, the federation-wrap correctness the unit tier cannot reach). Example README updated to show Graphitron.newGraphQL(). Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R99 (e6df34d): Widen the graphitron:dev LSP scan / walk to sibling modules when the goal runs from inside one sub-module of a multi-module reactor. Maven loads only the started module’s pom there, so getAllProjects() is a single project and the @service / @condition / @record classes in sibling modules were silently invisible: no completions, no hover, no goto-definition, no unknown-class diagnostics, with nothing to grep for. Fix detects the single-project reactor (singleProjectReactor()) and walks up to the nearest ancestor pom whose <modules> lists the current project, then folds each sibling’s target/classes into the scan side and its src/main/java plus disk-discovered target/generated-sources/* into the walk side, through one shared siblingModuleBasedirs() helper consumed by both resolveClasspathRoots() and resolveCompileSourceRoots() (the codegen reflection loader widens for free through the former). Both halves ride the same collectExistingDirs existence filter and dedup, so scan/walk parity (R351/R369) holds by construction: a sibling scanned for completion also has its source root walked. Sibling dirs are resolved by convention in declared <modules> document order (no Files.list, preserving catalog determinism) and no MavenProject instances are built for modules the session never loaded; a genuine standalone module finds no ancestor and is unchanged from pre-R99. DevMojo self-explains the single-module-no-siblings case rather than leaving the silent empty popup. Chose the parent-pom walk-up (Option A) over JAR-classpath scanning (Option B), which would have promoted the R369 unwalked-scanned residue onto the common path and crossed the scanner’s parse-only boundary; dependency-JAR consumers and non-standard sibling <build> dirs stay explicit non-goals. Coverage: unit-tier walk-up tests (document order, nearest-ancestor stop, no-ancestor-empty) and a mojo-tier single-reactor test asserting both that a sibling class lands in externalReferences() and (load-bearing for R369 parity) that its source root is walked; no code-string assertions on generated bodies. Docs: a "Multi-module projects" subsection in getting-started.adoc and a CLAUDE.md note beside the catalog-jar footgun. Independent-session In Review → Done review; full reactor green under -Plocal-db (AbstractRewriteMojoTest 13 tests). Builds on R351/R369.

  • R256 (2d13f72): Absorb the service walker substrate onto typed rejections + multi-arg ctors. R238 shipped ServiceMethodCallWalker as a behavior-preserving translator over an already-resolved MethodRef.Service, so most of the ServiceMethodCallError taxonomy it designed was unreachable: every reflection-time failure was produced upstream in ServiceCatalog as Rejection.structural(...) prose that lost its identity at the LSP boundary (Diagnostics.lspCodeOf returns null for Structural). This item migrates those failures onto typed arms that flow through the existing WalkerResult / Diagnostic substrate, keeping reflection at the parse-boundary reader (ServiceCatalog) rather than relocating it into the walker (the design fork the Spec resolved against per "classification belongs at the parse boundary"). Deliverable 1: a new ReflectionError sub-seal of AuthorError (graphitron.reflect.*) carries the reflection-intrinsic failures shared across the three reflect helpers (reflectServiceMethod/reflectTableMethod/reflectExternalField) — ClassNotLoaded, ReturnTypeMismatch (with a ReturnContext SERVICE|TABLE_METHOD discriminant selecting prose), ParameterNamesMissing, AmbiguousMethod — produced at the shared pickMethod / class-load / return-type sites so a @tableMethod failure of the same shape is not forced through a @service-named arm ("one predicate, one home"); the service-binding-specific arms (InstanceHolderUnconstructible, ArgumentParameterMismatch, DtoSourcesUnsupported, UnrecognizedSourcesType) re-land under ServiceMethodCallError (graphitron.service-method-call.*). Deliverable 3: checkServiceInstanceHolderShape relaxed to resolveInstanceHolder — resolves any public constructor whose params each bind from a DSLContext slot or a declared context key (legacy (DSLContext) still wins, (DSLContext, ctxArg) now resolves, no-arg admitted), CallShape.InstanceWithDslHolder carries the ordered ctorParams, the walker translates them into Instance.ctorArgs and raises MultipleDslContextSlots(CTOR) for a multi-DSL ctor; methods.get(0) replaced by pickMethod so an overload tie produces AmbiguousMethod instead of silently binding the first declaration-order match. Deliverable 4a: ConflictSite.site widened from a bare MethodRef to a sealed Site (Method | Carrier), retiring the ContextArgumentClassifier.syntheticServiceMethodRef sentinel that fabricated an empty MethodRef.Service just to satisfy the old field; ResolvedContextArg.sites carries the widened Site. Per the Spec’s split-it-out clause, the wire-coercion cast guard stays R261’s (R256 leaves scalarLeaf emitting as-is and only guarantees the typed-rejection channel), and deliverable 4b (bean-helper-queue ValueShape → synthetic CallSiteExtraction.InputBean round-trip) is carved out to R402 (retire-bean-helper-queue-valueshape-roundtrip). Coverage: unit-tier ServiceMethodCallWalkerTest (ctor-source translation in order, CTOR-round multi-DSL error), MethodRefCallShapeTest (multi-arg holder ctor + context-only-needs-no-dsl emit), ServiceCatalogTest; pipeline-tier ServiceRootFetcherPipelineTest (ReturnTypeMismatch SERVICE+TABLE_METHOD, AmbiguousMethod, multi-arg-ctor resolves without holder rejection) and ServiceFieldValidationTest (InstanceHolderUnconstructible) assert on typed arms and stable lspCode`s, no code-string assertions on generated bodies; drift guards `RejectionSeverityCoverageTest (a sample per new permit) and SealedHierarchyDocCoverageTest (typed-rejection.adoc paragraph + drift-list per permit) cover all eight new arms. Independent-session In Review → Done review; full reactor green under -Plocal-db (2310 tests; the 3 pre-existing R389 JoinedTableInheritancePipelineTest failures predate this work on an untouched classification path). Builds on R238; pins the boundary with R261; spawns R402.

  • R450 (cf2c34c + rework 1c0126d, Spec 7f7c35d/a92bbcb): Fix the split-path hop-0 condition filter binding the same alias as source and target. SplitRowsMethodEmitter.buildWhereCondition emitted a hop-0 condition: filter as method(firstAlias, firstAlias) (latent since the file’s creation; found in the R435 second-pass review), guaranteeing a javac incompatible-types error for concretely-typed filter parameters and silently self-referential SQL for wildcard ones ; and independent of the alias, the slot-tuple batch grain under-specified the fetch (two parents sharing an FK-slot value got one shared filter verdict). The fix makes grain and topology one decision at one producer: ParentCorrelation.OnConditionJoin generalizes to the parent-anchor arm OnParentJoin carrying only (firstHop, parentTable) with no condition() accessor (consumers dispatch the hop-0 attach on firstHop.on(): ColumnPairs → forward join, Predicate → two-arg condition call); BuildContext.buildParentCorrelation (single producer) lands any hop-0 filter() on that arm regardless of its On, keeping OnFkSlots only for filter-less FK/lifted heads; the batch grain becomes a projection off the arm (parentKeyColumns(): FK source columns / parent PK / routine inputs) read by deriveSplitQuerySource, so parent-PK grain iff parent-anchor topology is structural; the split emitter anchors parentAlias and binds it as the hop-0 filter source, with a terse classifier-unreachable throw under other arms; record/service split parents with a hop-0 filter reject via AuthorError.Structural naming the escape hatch (previously classified unverified ; Check 2 skips a null originTable). Same-commit consumer audit: three inline emitters re-dispatch on firstHop.on() (behaviour-identical), split-rows siblings share the parent-anchor path, TypeFetcherGenerator holds no ParentCorrelation switch. Coverage: pipeline (hop-0 filter → parent-PK sourceKey + OnParentJoin; hop-1 sibling → slot key + OnFkSlots unchanged; inline hop-0 filter → OnParentJoin), record-parent hop-0-filter Structural rejection fixture, OnParentJoin unit invariants + parentKeyColumns projection, and the execution-tier grain proof (two split_filter_parent rows sharing target_id=1 with opposite include values; split reproduces inline per-parent rows; the concretely-typed condition method also made the pre-fix double-bind fail compile-spec). Rework pass 1c0126d repaired the three stale terminal-back-walk javadocs in SplitRowsMethodEmitter (the R449-absorbed housekeeping). Independent-session In Review → Ready → In Review → Done review; full reactor green under -Plocal-db on both passes.

  • R451 (3ce199b, Spec 7ecdf89): Routine writes ; @routine on a Mutation field commits before the follow-up query. A Mutation field carrying @routine (a VOLATILE table-valued function) plus at least one @reference hop classifies as the new sealed leaf MutationField.MutationRoutineWriteField (verb Operation.RoutineWrite) and emits the DML two-step transposed onto the R435 chain: step 1 executes the routine inside dsl.transactionResult(...) (the R429 per-mutation-field boundary; commit on lambda return) capturing only hop 0’s key columns from the routine result, step 2 is a post-commit SELECT anchored on hop 0’s table with the captured keys, remaining hops joined forward, projecting the terminus type ; the routine never appears in step 2’s FROM, so the response always observes committed state. The (start, hops) chain shape extracted into the shared RoutineChain record (one invariant enforcer spanning read and write leaves, exposed via the RoutineChainField capability interface, ServiceField precedent); buildKeysInCondition generalizes the DML buildPkKeysCondition so both two-step fetchers share the composite-safe key-IN condition. JooqCatalog gained RoutineResolution.NonTableValuedRoutine (a verified probe of the generated routines sub-package) so a procedure or scalar/void routine defers to R454 (routine-write-result-shapes, filed ahead of the planSlug repoint) while a genuinely absent name keeps the structural rejection; the single-node Mutation @routine and a condition-joined or filtered hop 0 (no derivable post-commit re-read anchor) likewise land typed Deferred`s. Root-head and multi-routine rules extend to Mutation chains; sakila gained `public.rent_film and the scalar public.rental_count_for_customer (schema 2.8 → 2.9). Coverage: ClassifiedCorpus routine-mutation-write entry, GraphitronSchemaBuilderTest R451 block, RoutineMutationWritePipelineTest two-step fingerprint pin (sanctioned call-site form), and execution-tier rentFilm_* (commit observed by independent read; failing routine rolls back with nothing committed). @routine directive reference gained "Writes on Mutation". Independent-session In Review → Done review; full reactor green under -Plocal-db. Builds on R449/R435/R429; spawns R454.

  • R429 (f68666c/776f0d9/3a0f0dd/da38754/5bb881f/c2664aa + rework 7b86287/09ebe0d/171d468/ce5149d/11cf1e3): Graphitron owns the connection lifecycle ; application runtime, operation-typed transactions, and database-mounted session identity. An emitted application-scoped GraphitronRuntime owns the consumer’s DataSource; every operation pins exactly one connection (safe because batch loaders run SQL synchronously on the dispatch thread, tripwired at RowsMethodCallTest); the caller’s claims travel as an opaque String to a consumer-owned database connect hook at acquisition with a paired disconnect hook at release (fail-closed connect, evict on unmount failure, both hooks structurally outside any transaction: acquire normalizes autocommit before connect, release settles any open transaction before disconnect). Queries run in autocommit (blanket read-only enforcement split to R460); each mutation field commits or rolls back independently through the emitted GraphitronTransactionProvider (commit-policy axis: ROLLBACK_ONLY is R428’s rollback-everything dev mode); @defer stays off on the owned path (follow-on R469). <sessionState> emits the hook from config: function-hook callables with optional OUT-handle threading and the <stateSurvivesTransactions> survival opt-in (undeclared pairs re-fire per mutation-field settle through the provider’s opaque settle callback, so a settle can never leave stale or reverted identity; the read path is untaxed), or the Postgres <variables> sugar emitting both halves from one carrier (survives settles structurally; convention-fence warning with @service present; Oracle/RAS execution coverage is R468). Per-request entry is Graphitron.newOwnedExecutionInput(claims, ...) beside the R190 escape hatch (kept, with a one-time caller-owns-everything wiring notice); the tenant-keyed TenantConnections carrier lands the acquisition seam R45 consumes. Docs: runtime-extension-points.adoc rewritten for both paths, RLS-assumed principle, integrity gradient (enforced/convention/cryptographic fence), MP-JWT adapter recipe, hook state contract (session-scoped, never transactional). Sakila app adapter migrated as first client of the owned path. Independent-session In Review → Ready → In Review → Done review; full reactor green under -Plocal-db on both passes. Builds on R190; feeds R45/R428; spawns R460, R468, R469.

  • R428 (5488bc4/ae91f86/29420c8/ab56510 + rework 43de546/3758ec3, Spec fd33e86): MCP execute tool runs a GraphQL query/mutation against the generated resolvers in-process in the graphitron:dev JVM, closing the authoring loop (validate-error → compile-error → real result) with no app server. The load-bearing move is codegen, not a runtime seam: graphitron emits GraphitronDevExecutor into the output package, compiled in the same R410 pass, exposing one public static String execute(Connection, String dialect, String query, Map variables, String claims, Map contextArgs) whose signature is JDK-only, so the dev-loop host reflects exactly one method and no jOOQ / graphql-java type crosses the host↔generated classloader boundary; everything schema-varying (the newOwnedExecutionInput signature, the typed contextArgument binding, whether <sessionState> is configured) is absorbed at generation time. Inside, the executor wraps the host’s single dev connection in a one-connection DataSource, constructs the R429 GraphitronRuntime with the requested dialect, and runs under the ROLLBACK_ONLY commit policy so the dev loop exercises the same acquisition/hook/transaction path a real app does while never persisting a write. Host half (DevQueryExecutor in graphitron-mcp): a fresh platform-parented URLClassLoader per call over target/graphitron-classes first (R410 shadowing invariant) plus the consumer classpath, JDBC driver discovered via ServiceLoader on the project loader (DriverManager bypassed), TCCL pinned to the generated world for the call, executor-side failures (connect-hook rejection, fail-loud missing claims) surfaced verbatim. Config is a <devDatabase> block (url/user/password/dialect/claims/allowClaimsOverride) with env-wins overrides (GRAPHITRON_DEV_DB_*, GRAPHITRON_DEV_CLAIMS); explicit enumerated dialect (POSTGRES/ORACLE, never defaulted); absent url disables the tool quietly (RAG-style degrade); <sessionState> schemas fail loud on missing claims naming GRAPHITRON_DEV_CLAIMS; per-call claims override rejected unless opted in. R429 contract change flagged and reviewed: the ROLLBACK_ONLY arms of the generated transaction provider became a deferred observe-then-discard topology (operation transaction opens once and defers across field settles, each field boundary a savepoint, read-backs observe the writes, PinnedConnection.release discards everything) because the shipped R449/R451 DML two-step reads back committed state; the one stated fidelity limit (no mid-operation afterSettle re-fire under this policy) is documented in the provider/executor javadocs and pinned by a provider unit test. Federation _entities execution and opt-in commit are named follow-ons. Coverage: GraphitronDevExecutorGeneratorTest + ...PipelineTest (structural signature/gate/helper only, no body-string assertions), GraphitronTransactionProviderGeneratorTest (compiled-and-driven deferred topology), DevQueryExecutorTest / ExecuteToolTest / DevMojoTest / GraphitronMcpServerTest, and execution-tier DevExecuteExecutionTest (real Postgres: executor JSON byte-equal to a direct in-app execution, variables binding, observable-write + no-trace mutation, field independence, fail-loud/malformed claims). Independent-session In Review → Ready → In Review → Done review; both flagged findings (build-red on a missing mojo-configuration.adoc row for <devDatabase>; code-string assertions on generated bodies) fixed in the rework pass, which also corrected the spec’s false "driver is on the compile classpath" assumption by resolving ResolutionScope.TEST (the JDBC driver lives at runtime/test scope for plain/Quarkus apps). Full reactor green under -Plocal-db (independently re-verified, unpiped exit code). Note: the sakila-example POM’s new plugin-level <sessionState> (for CLI mvn graphitron:dev) is inherited by the federated/multischema generate executions via Maven config merge, so those fixtures now additionally emit a GraphitronSessionHook; harmless (build green) but a behavior change the landing commit described as "unaffected". Builds on R410/R429/R118.

  • R308 (1481592/0803628 + rework cbe34fb/324e69c/6791638, Spec 7517f93): Carrier arrival modeled on the @service payload seat ; one classify-time shape verdict over the triple (carrier field wrapper, producer return shape, data-field wrapper) replaces the uncoordinated wrapper reads that decided list-payload carrier admission. BuildContext.ServiceCarrierShape (Coherent(producerArrival) / Reject(typed error) / NotApplicable) is folded at FieldBuilder.scanServiceCarrierShape; producer arrival is decided once at the R96 reflection boundary and memoised per carrier-field coordinate (parentType.fieldName, not the payload SDL type ; two @service fields may return the same payload with different arrivals), and Coherent carries the required producer cardinality so checkServiceReturnMatchesPayload consumes the one fact instead of re-deriving it from wrappers (the read whose R329 data-field re-levelling false-rejected the coherent class-backed list carrier). ServiceCarrierShapeError is a new sealed sub-seal of Rejection.AuthorError with typed arms carrying the disagreeing SourceKey.Cardinality axes and stable graphitron.service-carrier-shape.* LSP codes: ProducerArrivalMismatch (list carrier, single-value producer ; subsumes the a1 silent runtime-crash admit and both misleading record-handoff rejections) and DataFieldArrivalConflict (list carrier whose @table-element or class-backed RecordElement data field is itself a list over a flat producer collection ; both per-request ClassCastException`s after a green build; the arm’s original `@table-only scoping was itself a defect the rework fixed; only an ID-element data field re-nests and stays coherent). Coherent shapes keep byte-identical classification and emit, now pinned: the @table single-data-field list carrier (pipeline + sakila execution fixture serviceFilmsByIdsAsPayloads proving exactly two SELECTs ; producer + one DataLoader-coalesced rows-method ; regardless of id count) and the class-backed single-composite list carrier (listCarrier_classBacked_collectionProducer_admitsCoherentComposite). The three in-code forward references for the ancestor-product arrival fold (ChildField.source(), Source.OnlyChild, WrapperAlgebraTest/ClassifiedDslTest pins) retargeted from R279/R308 to R463, which owns the fold. Independent-session In Review → Ready → In Review → Ready → In Review → Done review (three passes: missing coherent class-backed pin, whose writing exposed and fixed a third false reject; then prose drift from the arm widening); full reactor green under -Plocal-db on every pass, unpiped exit codes. Builds on R305/R316/R329; feeds R463.

  • R477 (cb42d90, Spec 15c679f): Batch entity dispatch rejects well-formed node ids whose decoded key has the wrong arity for a composite-key @node type, instead of crashing (under-arity) or silently resolving the wrong row (over-arity). The batch path (EntityFetcherDispatch, driving Query.node/Query.nodes and federation _entities) decoded a KeyShape.NODE_ID id in HandleMethodBody.emitDecodeAndGroup with a null-only guard and sized cols by the runtime decoded.length, while the paired SelectMethodBody indexes cols[i] for every column in alt.columns() (the fixed composite-key count): an under-arity id (e.g. Opptak:999 against a 2-column key) tripped ArrayIndexOutOfBoundsException inside select<Type>Alt<N> ; surfacing as a redacted 500 through node/nodes and as the raw, unredacted Index 1 out of bounds for length 1 machine-to-machine through _entities ; and an over-arity id silently resolved the row keyed by its valid prefix. Fix tightens the emitted guard to if (decoded == null || decoded.length != alt.columns().size()) continue and sizes cols from the same model fact, so a wrong-arity id is skipped exactly like a garbage/unknown id (null slot, empty errors), matching the Relay null-not-error contract, the opacity stance, and the single-record decode helpers that already guard values.length != N (NodeIdEncoderClassGenerator, InputBeanInstantiationEmitter). DIRECT alternatives are untouched (they size by requiredFields, contract-equal to columns for that shape). Fixture: the federated FilmActor gained @node + @nodeId (its _Entity union membership already came from @key, so the smoke test’s set is unchanged) to give the federation path a composite NODE_ID alternative. Coverage: execution-tier on FilmActor (composite PK actor_id/film_id) across all three entry points ; GraphQLQueryTest.node_compositeKeyWrongArityId_returnsNull and nodes_compositeKeyWrongArityIds_nullSlotsPreservePositions (under- and over-arity slots null, valid slot resolves, positions preserved), FederationEntitiesDispatchTest.entities_compositeNodeIdUnderArity_yieldsNullSlotNoError and entities_compositeNodeIdOverArity_yieldsNullNotWrongRow (the over-arity id’s real 2-part prefix (1,1) pins the wrong-row hazard, not just the crash); no pipeline snapshot pins the emitted guard text. Independent-session In Review → Done review; full reactor green under -Plocal-db. Explicitly out of scope and filed as R478: sealing KeyAlternative.KeyShape into Direct/NodeId variants (the prose-only invariant split that gave this bug a place to live).

  • R201 (b6ad9e5, Spec 2f946d1/1a9cb30/867a53a): Honor @field(name:) in @error payload construction shape resolution, the output-side mirror of R200 and the last leg of the @field symmetry set (R191 output reads, R202 @error extra-field reads, R200 input binding all Done). Construction of a class-backed error-channel payload previously read no @field: the mutable-bean arm matched set<UcFirst(sdlField)> on the raw SDL name and the all-fields-ctor arm assumed positional SDL-to-parameter alignment, so a payload whose Java member names diverged from the SDL field names had no remap despite the read side already honoring the directive. Resolution-side only (both emitter arms are selection-agnostic and untouched, R191 precedent). Gathering stays builder-internal: resolveErrorChannel folds each SDL field into a FieldBuilder-internal PayloadSdlField(sdlName, javaBaseName, fieldDirectivePresent) carrier (house idiom argString(f, DIR_FIELD, ARG_NAME).orElse(f.getName())), where the third component is load-bearing and cannot be reconstructed from the first two (javaBaseName == sdlName is ambiguous between no-directive and a directive whose value coincides with the SDL name; presence-tracking, not value-divergence, drives the ctor fork and the diagnostics parenthetical, exactly as the read side keys on hasAppliedDirective). Architect-reviewed against promoting the carrier to a model type: the sole consumer is this one classify-time site and only the resolved artifact (setter Method / ctor index) lands on the model. Bean arm: setter base is the directive value when present, SDL name otherwise, so a data-field directive participates in the shape’s existence check (a payload whose setter matches the SDL name while the directive names a different member now rejects, the one behavior change on previously-admitted carriers, pinned as an invariant). Ctor arm: one resolvedErrorsCtorIndex (name-matched under the directive when parameter names are available, record components always or POJO with -parameters; errorsFieldIndex otherwise) feeds BOTH collectDefaultedSlots and ErrorsSlot.CtorParameterIndex from a single source, since splitting them would place errors at the resolved slot while defaults fill the SDL slot (a mis-constructed payload that still compiles); unresolvable name and name-less-POJO reject with guidance rather than silently falling back to position. Blank @field(name: "") on any payload field rejects the channel (R200/R202 precedent); reject strings gain a (remapped to '<base>' by @field) parenthetical. Dead sdlFieldNames(String) helper removed. Coverage: unit PayloadConstructionShapeTest (remap admit, remapped-but-missing reject naming SDL field + directive value + parenthetical, camelCase of the directive value), pipeline ErrorChannelClassificationTest (bean-arm remap admit + data-field-directive existence reject, ctor-arm reorder admit at name-resolved index with defaults computed against it, rejects: unresolvable value, name-less POJO via a -parameters-free codereferences.noparams fixture package whose @field value coincides with the SDL name so a value-divergence shortcut would wrongly admit it, blank; regression floor: divergent names without a directive still reject as today), and execution-tier sakila round-trip through a @field-renamed setter-shape payload (error surfaces on the errors field, data fields arrive defaulted). Docs: field.adoc axis sentence + result-types.adoc payload-shape prose, paying off R202’s deliberately read-side-scoped prose. Independent-session In Review → Done review; full reactor green under -Plocal-db (unit 10, pipeline 29, execution 2). Builds on R191/R200/R202; R244 retired the former validator-pre-step consumer, leaving resolveErrorChannel the single live call site.

  • R502 (70dd329, Spec dd0b800): The fact "this type’s runtime carrier is a generic jOOQ Record`" reified once in the model as sealed intermediate `GraphitronType.JooqRecordCarrier extends ResultType permits JooqRecordType, JooqTableRecordType; ResultType’s permits become {JavaRecordType, `PojoResultType, JooqRecordCarrier}, so the permits clause is the single-sourced, compiler-closed membership of the carrier partition instead of an instanceof disjunction restated at six code sites and two javadoc sites (the drifted-allow-list smell). Converting sites: FetcherEmitter.propertyOrRecordBinding / inlineSuccessRead (by-name fallback arms), FetcherEmitter.isEnvDependentAccessorRead (negative consumption: carrier reads are source-only), SourceRowDirectiveResolver.rejectByParentShape (rejection-message fork keyed to the same fact the emitters act on), FieldBuilder.deriveAccessorRecordParentSource / derivePolymorphicHubSource (grouped switch arms collapse to case JooqRecordCarrier _), FieldBuilder.resolveRecordAccessor (complement guard reads the direct fact); javadoc groupings in FieldBuilder.resolveRecordAccessor and ChildField.PropertyField/RecordField repointed to {@link} the new symbol. The read strategy (typed Tables.X.COL constant vs DSL.field(name)) stays deliberately per-leaf; identity forks (GeneratorUtils.recordColumnReadArgs, resolvePolymorphicRecordParent, CatalogBuilder projections, TypeBuilder mint, EntityResolutionBuilder.kindLabel) untouched. Pure refactor, byte-identical emitted output; acceptance is the existing tiers plus compiler-checked exhaustiveness of every ResultType switch against the restructured permits. Independent-session In Review → Done review (census re-run clean; SealedHierarchyDocCoverageTest confirmed Rejection-scoped, no mapping needed); full reactor green under -Plocal-db.

  • R495 (d159757, Spec 9d1a788): The per-class javadoc InputRecordGenerator.buildClassSpec stamps on every generated input carrier told consumers "a build-time audit enforces this rule", but no such audit runs (it is a separate, still-Backlog item); the class-level javadoc correctly described it as deferred, so the two copies of the same fact had diverged (the drift smell, surfaced by the R483 javadoc audit). The emitted "Do not reference from service code" instruction now rests on the stated convention alone (the <outputPackage>.inputs package boundary and the stamped javadoc), with no forward-looking enforcement promise. One-line string-literal edit; no test pins the emitted wording per the code-string ban, golden-output review is the check. Independent-session In Review → Done review; full reactor green under -Plocal-db; regenerated sakila output verified free of the claim.

  • R497 (3de0c4f, Spec 9d1a788): The hand-maintained caller census in FederationSpec’s class javadoc ("Three callers reach for it: …​") carried one false entry (FederationLinkApplier, a `{@link}-only referrer that consumes the value indirectly via the registry) and one omission (ScalarTypeResolver), with nothing enforcing the list; the javadoc reference gate pins that linked symbols exist, not that they read the constant, which is why the census read as enforced while drifting (surfaced by the R483 javadoc audit). The class javadoc now states what the constant is and why the holder is neutral, and stops; find-usages is the source for the caller set. Single-file javadoc edit, no consumer changed, no bespoke caller-set enforcement added. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R443 (7816db7, In Review d2f4a61, Spec 83891f2): Replace the two-nullable-field BuildContext.ConditionResolution record, whose javadoc falsely asserted "exactly one of ref and error is non-null" while resolveConditionRef returned (null, null) in two places, with a sealed tri-state Resolved(MethodRef) / Failed(String) / Unresolved(). The record was the outlier among its own neighbours (FkJoinResolution, ConditionJoinTargetResolution, ArgBindingMap.Result/ParsedArgMapping are all already sealed switches), so sealing it removed ceremony rather than adding it: the tri-state contract is now compiler-enforced by an exhaustive switch at each of the three parsePathElement callers, replacing an ordered error() != null / ref() == null null-guard convention duplicated across those sites (the R438 review-fix that a fourth caller could have silently forgotten). resolveConditionRef’s returns map one-to-one (two `Unresolved, three Failed, one Resolved) and the false "exactly one non-null" javadoc is gone with no roadmap-id citation in its place. Behaviour-preserving refactor: identical messages reach the errors accumulator in identical cases, no generated-output change; acceptance is the existing pipeline coverage (QueryConditionsPipelineTest, TypeConditionsGeneratorTest, ReferenceFilterRemoteColumnPipelineTest) plus the compiler-checked exhaustiveness, no new code-string assertion on generated bodies. Non-goals held: no lift to typed Rejection (Failed(String) matches the local errors.add idiom), neighbouring sealed results untouched. Independent-session In Review → Done review; graphitron module and all named tests green under -Plocal-db (the sole reactor failure a pre-existing BgeEmbedderOnnxTest float-nondeterminism flake in graphitron-mcp, a module the diff never touches, passing on rerun). Post-R438 residue.

  • R503 (46f5d95/f55ec5a, In Review 03c2fc3, Spec 3bbc3d8/5d525d4): A directiveless output type reached both as a nesting projection of a @table parent (source is a generic jOOQ Record) and as a field of a class-backed producer result (source is the reflected backing object) now classifies both edges and is served by one datafetcher per coordinate that dispatches at run time on source instanceof org.jooq.Record, replacing the former deterministic validate-time rejection (the dissolved ConstructorField shape). The Backlog’s first-edge-wins/walk-order-race premise was refuted by audit and corrected in the Spec: the class-backed reach always won pre-walk and the nesting edge was rejected, so the real defect was the rejection blocking legitimate value-type reuse plus a latent name-keyed emitter overwrite. The reachable-source-shape union is reified once post-walk (MixedSourceReachIndex) as a per-FieldCoordinates fact on GraphitronSchema.reachableSourceShapes, read by the dispatch emitter and the validator’s shape-set rule so neither re-derives it; ReachableSourceShape carries the SINGLE_ARM/DISPATCHED/REJECTED partition. Classification: TypeBuilder.isNestingEdgeTarget admits a ResultType nesting target while NestingType registration stays gated on the strict isDirectivelessNestingTarget predicate, so the type-level winner is walk-order-independent (the nesting facts live edge-side on the NestingField`s). `FieldBuilder de-fuses the record-backed-child rejection from the type-level fact to the edge level; a child unresolvable as a column enriches the rejection by naming the binding producer. Emission: both merge sites (FetcherRegistrationsEmitter, TypeFetcherGenerator) merge per type name (no overwrite / duplicate TypeSpec) and FetcherEmitter.bindDualShape composes the two existing single-arm reads into a statement-form, Java-17-valid dispatch. Acceptances audit found a third consumer of the relaxed guarantee beyond the two known merge sites, the collectDomainReturnTypeConflicts DomainReturnType-disagreement pass, and suppresses it only for the supported class-backed mixed reach (sound: a genuine multi-backing-class or jOOQ-carrier conflict demotes the type to UnclassifiedType/JooqRecordCarrier and escapes suppression, still rejecting); the JooqRecordCarrier + nesting mix stays rejected in v1 via the reified shape-set rule. The spec’s "migrate the positive case to the classified corpus" cutover was deliberately not done (the per-field @classified dimensions don’t express the cross-edge shape-set union); the behavior is pinned instead by MixedSourceNestedTypeReadsTest. Coverage: pipeline (MixedSourceNestedTypeReadsTest: both walk orders, direct + two-hop, single-reach byte-identical), validation negatives (MixedSourceNestingReachValidationTest: column-only, accessor-only, jOOQ-carrier shape-set reject, two-hop supported reach validates clean), the ReachableSourceShapeCoverageTest shape-set partition meta-test, compilation tier (FilmBlurb sakila-example fixture under <release>17>), and execution tier (MixedSourceNestedTypeExecutionTest drives both source-shape arms through the one registered fetcher against Postgres); no code-string assertions on generated bodies (the emitted dispatch was verified by golden output). ConstructorFieldValidationTest retired, its positive fixture becoming the positive mixed-source case. Independent-session In Review → Done review; full reactor green under -Plocal-db (the sole failure the known pre-existing BgeEmbedderOnnxTest native-binding flake in graphitron-mcp, a module the diff never touches; the reactor passes with it excluded). Unblocks R501 (@pivot), whose coexistence mechanism can now repoint at the reified shape set. Builds on R502.

  • R501 (1fe1677, In Review ecb6e38, Spec 27b7e9f): @pivot(on:, value:, vocabulary:), a field-level directive turning a single-valued child field into a discriminator-keyed aggregate projection: one output slot per discriminator value, each holding max(<value>).filterWhere(<on>.eq(<token>)) over a narrow (owner-key..., discriminator, value) attribute table, generating declaratively the row-to-column pivot real subgraphs hand-write as a service (opptak’s OversatteTeksterService shape). Model: three new ChildField leaves split on the delivery axis mirroring the table pair ; PivotField (inline correlated aggregate subselect folded into the parent query, no GROUP BY, no DataLoader), BatchedPivotField (@splitQuery: the DataLoader seam with a key-preserving LEFT JOIN from the parent-input VALUES table and GROUP BY __idx__, scattered single-per-key), and PivotSlotField (one fact: the derived read name) ; composing a shared PivotSpec (single-FK-hop path pinned by compact constructor, on/value ColumnRef`s, projection type, slot → token map resolved at classify time from the `vocabulary: enum’s @field(name:) values or by identity), plus an Operation.Pivot verb and a FieldClassification.Pivot LSP projection. No new type variant: the projection type registers as the ordinary NestingType behind the existing first-wins guard, and the pivot edge joins the nested-type fetcher wiring, MixedSourceReachIndex, and the R503 dual-shape dispatch, so one registered fetcher per slot coordinate serves the pivot record, a compatible nesting parent’s row, and a class-backed @service result. Invariant: one projection record exists per parent, always ; absence surfaces as null slots, never a null record, on both deliveries (the split path’s left join is the one deviation from the batched table shape’s inner join; v1 rejects multi-hop and condition-join paths to keep the chain key-preserving). Composite owner keys supported from the start (arity-generic AND-chain correlation). Validation: PivotError, a 12-arm sub-seal of Rejection.AuthorError with stable graphitron.pivot.* LSP codes (non-null / non-scalar / divergent slots, vocabulary misses, unresolved columns, value-type mismatch, list return, unsupported path, record-backed and root parents ; the record-parent message deliberately not suggesting the lint-ignored @splitQuery); the duplicate-token invariant fires at validate time on the classified leaf. Coverage: the pivot corpus example, PivotClassificationTest (spec payloads, identity + vocabulary maps, composite keys, mixed-source shape-set union in both registration orders), PivotValidationTest (one negative per rejection arm), sakila fixtures (film_translation/film_actor_note/film_price/pivot_nesting_host + PivotTextsService), and PivotExecutionTest against PostgreSQL (slot placement, null slots, row-less parents on both deliveries, inline/split parity, selection gating, aliased duplicates, composite keys, and the three-source-shape coexistence). Docs: docs/manual/reference/directives/pivot.adoc, typed-rejection.adoc PivotError section. Out of scope, named for later items: aggregates other than max, multi-value-column projections, non-equality discriminators, multi-hop paths (chain-wide key preservation), record-backed parents, mutations. Independent-session In Review → Done review; full reactor green under -Plocal-db. Builds on R502/R503.

  • R504 (2b84562, In Review a81e30e, Spec 75eeaed/2400879): Scrubbed the dead ChildField leaf taxonomy (SingleRecordTableField, RecordTableField, SplitTableField, RecordLookupTableField, SplitLookupTableField, LifterLeafKeyed, RecordTableMethodField) from present-tense comments, javadoc, and test-method names across graphitron main and test sources, the sakila-example SDL and execution tests, and one sakila-service fixture: each claim repointed to its live successor (the record-/table-sourced BatchedTableField / BatchedLookupTableField arms, KeyLift.Lifter, TableExpr.MethodCall) or deleted where it restated the code, deletion-first per the spec; historical "former/collapsed/dissolved" narrative kept intact. Co-located stale claims fixed in the same pass: the lifted single-cardinality record-parent rejection (formerly Invariant #10) no longer described as live, the retired PojoResultType.NoBacking promotion claim replaced by the actual RETURNING-table JooqTableRecordType binding, Wrap.TableRecord/Reader.ResultRowWalk key mechanics repointed to KeyLift.ProducedRecords, and the @lookupKey directive description stripped of internal leaf names and a roadmap-id citation. Test-class names (SingleRecordTableFieldServiceProducer*Test) and lowercase scenario-style method prefixes kept as scenario names per the item’s default. No production-model, generated-output, or test-behaviour change; the item’s authoritative sweep now returns only historical residue, and SingleAccessorOnListField was confirmed live (a declared test dummy), not dead vocabulary. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R45 (e6b955a..5364cbf slices 1-6, rework f8027fd; Spec 1613680, first In Review 5364cbf, rework requested a23ab82, second In Review 0468f03): Operation-divined tenant routing: with one Mojo element (<tenantColumn>) every catalog table classifies tenant-scoped or global (type read off the jOOQ column, disagreement and unknown-column typed rejections), and every field gets a sealed TenantBinding arm computed once post-walk from the column mappings the schema already carries (ArgumentBound with co-binding set and runtime agreement guard, per-row NodeIdBound/EntityRepBound, Inherited via graphql-java localContext hand-down, Untenanted), so the generated fetchers acquire the right per-tenant DataSource through the R429 TenantConnections carrier with no @tenantId directive and no request-scope tenant parameter. Every fan-out point partitions per tenant: node/_entities dispatch groups per (alternative, decoded tenant) through the shared resolveByReps seam, DataLoader identity joins an opaque tenant segment through a single generated naming seam, and in multi-tenant builds ConnectionResult carries the fetcher’s routed DSLContext so the lazy totalCount/facets aggregate against the source the page rows came from. Unroutable tenant-scoped fields reject at build time (noTenantBinding), including cross-scope reaches (one statement touching tenant-scoped and global tables) and the non-Record shapes (multi-table polymorphic participants, pivot attribute tables) after the first review pass caught their silent Untenanted fallback; the second pass verified the honest-reach classifier, the deleted localContext-presence heuristic, and rendered the multi-tenant connection emission end-to-end. Coverage: TenantScopeClassifierTest, TenantBindingClassificationTest (all arms plus the four non-Record shapes), TenantScopeValidationTest, TenantRoutedFetcherPipelineTest/TenantRuntimeKeyTypeTest (typed key on every surface, loader-name partition, both dispatch decode shapes, single-tenant byte-for-byte baselines), the multitenant.graphqls L5 fixture, and TenantDivinedRoutingExecutionTest against real database-per-tenant PostgreSQL (isolation, inherited batch routing, unknown-tenant pre-SQL error, mutation routing, cross-tenant nodes batch partitioning). Accepted coverage shape: _entities wire-level execution rides the nodes proof through the shared dispatch surface, with the @key field-value decode pinned at pipeline tier; a federated multi-tenant execution fixture stays open as follow-up. Independent-session two-pass In Review → Done review; full reactor green under -Plocal-db on both passes. Deliberately deferred, recorded on siblings: fan-out for deliberate no-binding shapes (R46), tenant-index parent-row routing (R505), per-element localContext stamping below per-row dispatch surfaces (spec open question 5, degrades closed).

  • R489 (0ef2353, rework 1f8340e; Spec 220db01, first In Review 5e14f49, rework requested 9576465, second In Review 77d9e9c): Normalized the DML reentry correlation onto the VALUES-join primitive: the rows<Name> companion (the projected / discriminated mutation’s follow-up SELECT) resolves its correlation through one seam at two cardinalities ; the bulk arm renders the shared VALUES (idx, key...) derived table (ValuesJoinRowBuilder core) joined over the carried correlation and ordered by idx, the single arm the legible degenerate, plain key equality, byte-identical to the retired keys-IN spelling. The classifier attaches the PK-self-identity ParentCorrelation.OnLiftedSlots fact to the four DmlReturnExpression reentry arms (constructed once in FieldBuilder.buildDmlReturnExpression; emit sites read the carried fact, never re-derive the key set), rejects a @table DML return on a PK-less table at classify time (model-tier pinned over film_list at both cardinalities after the rework pass), and validateDmlReentryKeyArity mirrors the Row22 cap on bulk reentry keys at validate time. Two pieces of undefined bulk behavior became an execution-tested contract: payload rows align one-to-one, in order, with the rows the write reported through RETURNING (row-per-write, not row-per-distinct-key) ; pinned by the createKeyedNodes input-vs-key-order fixture, the missed-row cardinality shrink, a direct rowsUpdateFilms duplicate-keys companion-seam call, and discriminated bulk order + __typename routing over createContents. buildPkKeysCondition deleted; buildKeysInCondition narrowed to the routine-write step-2 re-read as its sole sanctioned caller. User manual (mutation.adoc, tutorial 05) reconciled to the shipped two-step write-then-read-back emit with the ordering / cardinality contract sentence. Independent-session two-pass In Review → Done review; full reactor green under -Plocal-db on both passes.

  • R508 (e7e5e56/7c9bb82/43a476f/5a84b88 slices 1-4, review fix 63aadb8; Spec 6db96fc, review pass 238b6a7, Ready 64e6a2d, In Review 80d1722): Dissolved the composite-column leaf family on all three classification axes: the CompositeColumnField/CompositeColumnReferenceField pairs on ChildField and InputField and the CompositeColumnArg/CompositeColumnReferenceArg pair on ScalarArg merged into their arity-1 siblings under fresh names (ColumnBackedField/ColumnBackedReferenceField per field axis, ColumnBackedArg/ColumnBackedReferenceArg), so arity is a columns count (1..N) on one leaf per shape instead of a leaf dimension; six leaves retired. Compact-constructor invariants pin the shape (non-empty defensively-copied columns; composite implies NodeIdEncodeKeys/NodeIdDecodeKeys, documented as a deferred-generalization seam with the Direct-implies-arity-1 corollary; ParentCorrelation.checkCarrierInvariant unconditional on the output reference arm), and arity is classified once on the derived isComposite() accessor that every consumer reads. The rooted-at-parent NodeId reference deferral moved enforcer in the same commit as the stub removal: GraphitronSchemaValidator.validateColumnBackedReferenceField rejects NodeIdEncodeKeys compaction as Deferred at every arity (the arity-N half is a new validate-time rejection replacing the generator stub), keyed by the merged class through Rejection.StubKey.VariantClass, leaving TypeFetcherGenerator.STUBBED_VARIANTS empty; the merged non-reference output leaf landed in IMPLEMENTED_LEAVES, the reference leaf in PROJECTED_LEAVES, with GeneratorCoverageTest’s partition exhaustive-and-disjoint throughout. The INSERT carve-out became arity-gated with the rejection re-worded to arity phrasing; the 22-slot `RecordN cap survived as conditional validator checks; FieldClassification.CompositeColumn* projection variants kept wire-stable, re-derived from isComposite() in CatalogBuilder. Coverage: ColumnBackedFieldInvariantTest/InputColumnBackedFieldInvariantTest/ColumnBackedArgInvariantTest pin the constructor invariants per axis; NodeIdPipelineTest gained validate-tier assertions that both arities of the rooted-at-parent reference reject as Deferred on the merged StubKey anchor. Generated sakila output byte-identical through the slices; the review fix then dropped the composite-aware arm-ordering pin in TypeClassGenerator (permanent generator complexity serving only the transition gate), accepting a behaviorally-inert two-file arm-order diff proven equivalent at the execution tier. Docs regenerated and R24/R27/R419/R462/R333 re-anchored to the merged carriers. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R507 (999cc6c; Spec b7823c9, Ready 023e2bf, In Review b2d3d07): Documentation staleness prevention landed as one escalating pipeline (declare, sweep, promote). The "Documentation names only live tests/code" corollary in development-principles.adoc rewritten affordance-first: every prose claim about code is anchored on a checked {@link}, pinned by a named live test or type, or written at intent altitude, with orientation javadoc blessed as the intent-altitude case (GraphitronSchemaBuilder’s top comment the exemplar) and deletion preferred over rewrite; "Principles are stated at altitude" now names the regenerate-and---verify` materialized view (supported-directives.adoc precedent) as first choice for any enumerable doc surface. roadmap/workflow.adoc gained the ## Retired vocabulary item-file convention (the fresh-context Done reviewer’s grep query, rot-safe by Done-deletion) and the reviewer-side "Retirement sweep" check, with the agent-facing trigger line in CLAUDE.md. RetiredVocabularyGuardTest promotes recurrently-surviving terms to a build gate: a 14-token registry seeded from the completed vocabulary scrubs, each entry carrying its live successor for the failure message, whole-identifier-token matching so live compounds never match, habitats comment/javadoc regions (main and test), main-source string literals, authored docs/ AsciiDoc, and fixture SDL, plus a reverse-enforcer failing the build when a registered token reappears as a main-source code identifier (stale entry or revived name). Seeding visited every surviving lineage mention and deleted or rewrote it present-tense, so the reviewed (file, term) allowlist ships empty. The region lexer extracted to JavaSourceRegions (new code projection pinned by JavaSourceRegionsTest; comment/string projections still pinned through RoadmapReferenceScannerTest), walk scope shared via GuardScope so the two prose guards cannot drift apart. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R513 (259cdff, In Review 6d9ea8c; Spec cee5724, Ready 5486708, In Progress 04d21fd): Fetchers helper names collided when two schemas expose a same-named table: jOOQ generates two record classes with an identical simple name but distinct packages (multischema_a.…records.EventRecord vs multischema_b.…records.EventRecord), and a *Fetchers class binding both (cross-schema @service mutations) emitted two createEventRecord helpers with the same signature and failed to compile, because every helper-name site keyed the stem on ClassName.simpleName() and deduped only within a full-ClassName group. Introduced FetchersHelperNames, one resolver per *Fetchers class and the single home for the create* (jOOQ record + bean) and decode* method-name namespaces: the create* stem set spans the union of the jOOQ-record carrier classes and the collected bean classes so cross-family collisions are caught too. A class with a unique simple name keeps simpleName() (the common single-schema case is byte-for-byte unchanged); a colliding class gets a per-class package-derived prefix (schema segment for jOOQ layout, last segment otherwise), pascal-cased, extended right-to-left and finally by a full-class-name-ordered ordinal if still ambiguous, with uniqueness enforced over the emitted method names (singular and plural, catching the create<A>List-vs-bean-AList overlap). JooqRecordHelperNames keeps its shape-contention machinery but now draws each class’s base stem from the cross-class create* map, so cross-class disambiguation and within-class shape ordinals compose (createMultischemaAEventRecord1/2). The three uncoordinated static "create"/"decode" + simpleName() derivations in ServiceMethodCallEmitter and InputBeanInstantiationEmitter are deleted and every naming site threads the resolver, so no naming decision survives outside the single home; bean-class and decode-record collection hoisted up front so the resolver is built before any body emits. A generation-time backstop asserts no two methods on the built *Fetchers TypeSpec share a signature, turning any future bypass into a loud generator failure instead of uncompilable output. Coverage: unit tier (FetchersHelperNamesTest: unique/jOOQ-layout/bean/union/List-overlap/extend/decode/order-independence/routing-hole), pipeline tier against the real multischemafixture catalog (FetchersHelperNameCollisionPipelineTest: distinct helpers, per-mutation routing, collision-times-contention composition, single-schema no-churn pin), and a new rewrite-generate-multischema-mutation example slice binding both EventRecord`s through service methods of distinct record types so the `-release 17 compile proves both compilability and routing. CompositeDecodeHelperRegistry and the GraphQL-type-name-keyed registries correctly untouched (their names cannot collide across schemas). Independent-session In Review → Done review; full reactor green under -Plocal-db. Sibling to R512 (cross-schema @reference(key:)), independently landable.

  • R512 (4976399, In Review 07c95f8; Spec 1ad4b3e, Ready 0b32af9): A @reference(path: [{key: "<fk>"}]) foreign-key name resolved through JooqCatalog.findForeignKey across every schema in one flat namespace, so when the same SQL constraint name existed in two or more schemas and source scoping could not break the tie (source null, non-table-backed, or itself ambiguous), the lookup returned Ambiguous and the author had no lever to disambiguate: unlike @table(name:)’s `schema.table form, the key: grammar had no schema-qualified form, making a cross-schema constraint-name collision unfixable. This bites real multi-schema databases (Samordna opptak) and the reactor’s own multischema fixture reproduces it (multischema_a.note and multischema_b.note both hold note_event_fk). A key: value may now carry a leading schema. qualifier (multischema_a.note_event_fk) scoping the FK candidate set to the FK-holder (child / referencing) schema, symmetric to the @table precedent, with unqualified behaviour byte-for-byte unchanged. Copies the @table shape, not just the grammar: a distinct QualifiedForeignKeyName(Optional<String> schema, String name) carrier plus parseQualifiedForeignKeyName parsed once at the author-string entry points (so the two non-author-facing internal callers keep the two-arg findForeignKey and stay visibly exempt from the grammar), and a schema-scoped findForeignKey(name, source, schema) overload. Scoping precedence is qualifier-hard / source-scope-soft: the qualifier filters on fk.getTable().getSchema().getName() (case-insensitive, exactly as two-arg findTable matches its schema half) and an author scope that eliminates every candidate is NotInCatalog for that schema, not a fallback; the soft source-scope then narrows only when it leaves something, so a within-schema duplicate constraint name (PostgreSQL scopes constraint names per table, not per schema) flows into the existing distinct-then-Ambiguous ending rather than a silent first-hit. The "does not connect" connection check was lifted out of the {key:} path element into one shared foreignKeyConnectionRejection enforcer firing uniformly at both author-facing sites (the path element and the explicit record-FK site resolveRecordFkTargetColumns), since a qualified key can now name an FK in a different schema than the source, making "resolves but does not touch" reachable at both. The LSP Diagnostics.validateCatalogFk mirror strips the qualifier before its bare-name match, re-sourcing the split from the shared parser (not re-implementing the grammar) so a valid qualified key is no longer red-squiggled; the bogus-schema arm stays deferred (the snapshot carries no per-FK schema to test against). Fixture: a within-schema duplicate dup_gizmo_fk on two multischema_a tables pins that (schema, name) is not unique, and the init.sql comment wrongly claiming Postgres constraint names are schema-scoped is corrected. Coverage: unit tier (JooqCatalogMultiSchemaTest, the seven-case matrix incl. holder-vs-target on the genuinely cross-schema gadget_widget_id_fkey and the within-schema duplicate ambiguity, plus the parser battery and the two resolveRecordFkTargetColumns arms), pipeline tier (QualifiedReferenceKeyPipelineTest: correct-schema resolves and lands the join, wrong-schema and no-such-FK reject), and LSP tier (DiagnosticsTest: valid qualified key not flagged, unknown bare name still flagged). Docs: the ReferenceElement.key directive doc string and the @reference / @referenceFor reference pages document the schema.constraint form. Independent-session In Review → Done review; full reactor green under -Plocal-db (jOOQ regenerated from the fixture bump). Sibling to R513 (cross-schema Fetchers helper names), independently landable.

  • R514 (96ccb28, In Review eed4ac0; Spec 76e8cdf, Ready 26ff511, In Progress ae67d35): A payload-returning @mutation(typeName: DELETE, table:) field rejected with the generic "return type not yet supported" the moment the deprecated @table moved off its input type, so every error-channel-payload DELETE was a dead end for the @table-on-input deprecation warning (all five in fs-plattform’s opptak-subgraph). Root cause: the binding-walk grounder RecordBindingResolver.groundDmlMutationField read the write target only off the single @table-bearing input argument, so a field-derived DELETE grounded no ProducerBinding.DmlEmitted, the payload never registered as a producer-backed carrier, and the return classified down the ScalarReturnType arm into the fall-through rejection; the field-level @mutation(table:) write target had been wired into the input/WHERE side but not the grounder. The fix single-sources the write-target precedence in a phase-portable helper MutationInputResolver.resolveDmlWriteTableRef (@mutation(table:) preferred on a verb in TABLE_ARG_SUPPORTED_VERBS, else the input’s @table, resolved through the catalog), consumed by both the grounder and the classify-time FieldBuilder.resolveDeleteWriteTarget, so a grounded DmlEmitted and the classifier’s write target cannot disagree. The verb gate moved to the helper’s home and is now read by the classifier’s unsupported-verb guard, the grounder, and graphitron:validate alike; arrival cardinality lifts from the field’s single input-object argument (the @table input on the deprecated bridge, the raw input on the field-derived route). An unresolvable or absent target stays a silent skip in the grounder; the loud diagnostics (unknown-table, no-write-target) remain resolveDeleteWriteTarget’s, pinned on the payload dispatch arm. Coverage: `MutationTableArgClassificationTest (single + bulk payload DELETE classifying equivalently to the @table-on-input route on the classified-model verdict, both-present precedence with the grounder proven to bind the field’s table and not the input’s disagreeing one, unknown-@mutation(table:) loud rejection on the payload arm), DmlBulkMutationsExecutionTest round-tripping a field-derived payload DELETE (deleteStorageBinByCodePayload) against PostgreSQL. Independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R46 (slices a0a1b36/83cc748/4fdebb1/2746c5e, self-review 8c308cf, rework 0fccc23; In Review 7d62453/0933b96, rework requested a68a8e7): Multi-tenant fan-out: directive @tenantFanOut on FIELD_DEFINITION runs a field’s query once per tenant in the request’s domain, in parallel, and unions the results into one list. The runtime’s first concurrency, confined to one generated helper: TenantConnections.scatter(keys, perTenant) runs each distinct tenant’s unit of work on a bounded executor (two flat GraphitronRuntime constructor scalars, fanOutConcurrency default 8 and a 10s deadline, plus an optional consumer Executor), joins with the deadline, and reports a sealed per-tenant Outcome taxonomy (Success/Failed/TimedOut), policy-neutral about partial failure; fetchers stay synchronous, workers resolve connections only through dslFor(key), and the thread-safety rework (concurrent pinnedByTenant with per-key single acquisition, straggler quarantine routing possibly-live connections through Connection.abort, covering the timeout, interrupted-join, and mid-submit-rejection paths alike, re-entrancy guard) keeps the one-connection-per-operation story intact. Classification adds the deliberate no-binding FanOut arm to the sealed TenantBinding with a closed validate-time rejection ladder (mutation, @service/@tableMethod/@routine, @lookupKey, @asConnection, the polymorphic family, non-list shape, global-only reach, existing binding, any-path tenant-bound ancestor, nested marker, v1 parent-shape boundary, single-tenant builds) plus a completeness sweep over objects and interfaces guaranteeing every marker application ends as a verdict or a located rejection; the three remaining TenantBinding consumer forks converted to exhaustive switches. Emission: the fanned fetcher scatters its ordinary jOOQ statement per tenant, unions in deployment-stable domain order, stamps each row’s tenant as per-element DataFetcherResult localContext (so Inherited children and @splitQuery loader-name partitioning compose unchanged), and collapses failed tenants into one appended null element each plus a path-bearing redacted error with extensions.classification (TenantFanOutFailed/TenantFanOutTimedOut); both factory forms gain a dedicated Collection<tenantKey> parameter exactly when a fanned field exists, and the fan-out domain is the configured tenant map intersected with the request’s collection (claimed-but-unhosted fails pre-SQL, hosted-but-unclaimed is silently never queried). SDL element nullability composes the partial-failure strictness; no config knob. Docs: the @tenantFanOut reference page and the database-per-tenant runtime how-to. Coverage at four tiers: emission pins (TenantRuntimeKeyTypeTest), direct-carrier concurrency proofs over fake JDBC (TenantScatterSubstrateTest: cap, deadline, failure isolation, outcome order, per-key contention, re-entrancy, straggler/interrupt/rejection quarantine), classification (TenantFanOutClassificationTest, 22 tests incl. the interface-field sweep and the any-path mixed-path fixture), pipeline (TenantFanOutFetcherPipelineTest), and real per-tenant PostgreSQL execution (TenantScatterExecutionTest, TenantFanOutExecutionTest: domain-order union with per-tenant ORDER BY, the batched form, @splitQuery per-tenant partitioning, pre-SQL unmapped-tenant error, authorization pre-filter via connection counts, downed tenant under [Thing] and [Thing!], timeout classification end to end). Follow-ups filed: R517 (argument narrowing), R522 (seam-pin assertion convention), R523 (fold rejection source locations). Two-pass independent-session In Review → Done review; full reactor green under -Plocal-db.

  • R524 (bouts 6bddc73/dbd3ccb/cdabe6e/ec86c95/6c89e93, In Review 564c561; Spec 7e8ec4a`+f9267ce, Ready `9438f81, In Progress 01b7a2e): Comment-verbosity alignment sweep over the hand-authored trees of the in-scope modules, bringing the pre-convention comment stock in line with the terse-and-pinned javadoc conventions (CLAUDE.md "Javadoc conventions"; "Documentation names only live tests/code" in development-principles.adoc). Unit of verdict was the claim, not the block: transition narration and future-work promises deleted or restated present-tense, code restatement deleted or reduced to {@link}`s, load-bearing claims kept terse and pinned (reference-gate `{@link}`s, named tests, published docs), orientation on-ramps preserved and tightened, never manufactured (that is the source-orientation item’s charter). Census-ranked worklist of 241 files (cutoff: >= 100 comment lines or a 25+ line block), 238 workable after the drift-follow-on exclusions, every file above the tail ledgered in the spec body as edited-with-counts or no-trimmable-claim; `BuildContext rejoined mid-sweep when its exclusion owner (R494) landed. Each bout passed an adversarial verify stage prompted to restore deletions; it caught one wrongful deletion in bouts 1, 2, and 5 and one misattributed rewrite in bout 5, all corrected before landing. Dozens of verifiably stale claims fixed in passing (dangling {@link}`s, wrong method/arm/count cites, contradicted contracts), each verified against live code rather than rewritten as confident prose; claims that could not be cleanly pinned or deleted were routed out, not rewritten: comment-drift corrections to R526, unpinnable load-bearing contracts to R527, emitted-text findings (string literals untouchable under the sweep’s comment-only constraint) appended to R521, plus R525 (filed and shipped mid-sweep). Edits comment/javadoc-only across all five bouts (mechanically verified at review). Census moved 58917 → 54838 comment lines (24.9% → 23.6%), context only, no numeric gate. Independent-session In Review → Done review; full reactor green under `-Plocal-db with the {@link} reference gate and RoadmapReferenceGuardTest active.

  • R51 (b8be7ec, In Review a39df32; Spec ed3a88a`+94a6d25, Ready `7299728, In Progress eccc4fc): Merged ChildField.PropertyField and ChildField.RecordField into one record-read leaf, ChildField.RecordReadField, under the fresh name that forces every switch arm and instanceof through the compiler. The two leaves were one leaf wearing two names (same source fact, same empty operation set, same read mechanism), and the emit layer proved it: both bind arms dispatched to a shared propertyOrRecordBinding, four more two-arm instanceof chains extracted the same slots from either leaf, both TypeFetcherGenerator arms were identical no-ops, validatePropertyField was an empty method pointing at a nearly-empty validateRecordField, and the LSP projection already merged them onto one FieldClassification.RecordOrProperty label with byte-identical arm bodies. What actually differed was carried facts, not identity, so the nullable columnName/column/accessor triple became one non-null sealed component, ValueLocator, with four arms per the real populations: TypedColumn(ColumnRef) (typed Tables.X.COL read off a table-record parent), JavaAccessor(AccessorResolution.Resolved) (class-backed parent), ByName(String) (untyped DSL.field(name) read on a JooqRecordCarrier), and DefaultRead(String) (graphitron locates nothing: @error-type parents and class-backed parents whose backing class did not load, the two both-slots-null populations that would otherwise have overloaded ByName). Three model changes rode along, each deliberate and challenged rather than assumed. target() now derives listOrSingle(returnType.wrapper(), Field) unconditionally: the scalar path gains the ReturnTypeRef.ScalarReturnType it did not carry, so a list-shaped scalar such as an @error type’s path: [String!]! obeys the SDL-list mirror, the error-field/error-type corpus rows flip target: Single to List, and the merged leaf exits WrapperAlgebraTest’s `CARDINALITY_NOT_MODELED exemption (now four members, the column family and ErrorsField, which still carry no wrapper slot and stay pinned deliberately). domainReturnType() derives from an exhaustive locator switch, dissolving the drift where the same resolvable column answered its Java type on one leaf and Object on the other; the change is diagnostic-visible through MultiProducerDomainTypeDisagreement, with no corpus entry gained or lost. And the empty validator pair became a real cross-axis gating rule, validateRecordReadField, checking each locator arm against the parent type’s source-object shape, which is what lets FetcherEmitter.recordReadBinding and inlineSuccessRead become exhaustive locator switches whose per-arm parent casts are guaranteed by a checked fact instead of construction-site coincidence (the composition rule: the source object gates and casts, the locator locates). Both read sites had independently reconstructed the same three-way fork as instanceof cross-checks against nullable slots. Corollary cleanups: isEnvDependentAccessorRead’s `JooqRecordCarrier early-return deleted as structurally impossible once the accessor lives on an arm, and inlineSuccessRead’s broad defensive terminal `throw narrowed to a single DefaultRead arm. Implementer decisions beyond the spec: DefaultRead registers PropertyDataFetcher.fetching(name) explicitly (mirroring the ErrorsField PayloadAccessor arm) so "graphql-java’s default machinery applies" is an emitted value rather than absence-of-registration, and joins resolvesViaPropertyDataFetcher, making the DefaultRead-under-Outcome corner a validate-time rejection; and the LSP RecordOrProperty projection now derives per locator arm, so a JavaAccessor read projects its accessor member name with a null columnName instead of faking a "Column:" hover label. Coverage: RecordReadFieldValidationTest (unit tier, one positive and one negative case per constrained arm plus the unconstrained DefaultRead), RecordFieldAccessorValidationTest renamed to RecordReadAccessorValidationTest with the two single-purpose validation tests collapsed into it, and corpus rows plus the graphitron-sakila-example compile as the right tiers for a representational change (no per-arm unit tests, no code-string assertions on generated bodies). GeneratorCoverageTest’s two `IMPLEMENTED_LEAVES entries became one; generated sakila output byte-identical; supported-schema-shapes.adoc and inference-axis-coverage.adoc regenerated. Deliberately out of scope: folding NestingField’s passthrough read and `ErrorsField’s `Transport localContext arm-set onto ValueLocator (when they join, locator() lifts from a leaf component to a capability interface and the exhaustiveness bookkeeping relocates), and the type-level source-object fact. Follow-up filed: R537 (pin the DefaultRead-under-Outcome rejection, whose absence leaves inlineSuccessRead’s unreachability claim unpinned). Independent-session In Review → Done review; full reactor green under `-Plocal-db.

  • R527 (4df1008`+27eec25, rework `db67c3e; Spec 29feac9`+01dec0f, Ready `c491c60, In Progress d6bc2e2, In Review fa12d41, rework requested 054959e, re-review fc38a2c): Resolution of the five load-bearing comment contracts the R524 verbosity sweep could neither delete nor pin, each resolved as an enforcer or a correction rather than a fourth restatement ("Every invariant has an enforcer", development-principles.adoc). Claim 1 is the one that paid off most: the spec’s own hypothesis (graphql-java coercion drops an explicit-null field from a nested input-object value, narrowing the top-level present-null/omitted/value three-way to a nested two-way) was probed and found false, and the four consequences already execution-pinned in GraphQLQueryTest were all telling that false causal story. Coercion retains a nested explicit-null at every depth, variables and inline literals alike; the observed collapse is jOOQ-side, the identity group’s trailing fromArray key decode resetting the touched flag of every null-valued column record-wide (Record.from() null-skip semantics) and erasing the earlier NULL write. customerUpsert_explicitNullNestedLeaf_noIdentityDecode_writesNull pins the isolated counterpart (no decode, the NULL write survives), the openDescent/emitColumnBinding/emitKeyDecode javadoc plus the two false test/service comments now state the verified mechanism, and the behaviour question (should a key decode stop erasing an explicit-null write, given a column write’s fate currently depends on an unrelated sibling field’s presence?) is routed to R534 rather than resolved drive-by. Claim 2 drops the phantom "per-arm LSP fix-it for ClassNotFound`" cite, repointing `resolveFromDirectiveValue’s rationale at `ParsedDirectiveValue.Malformed/parseDirectiveValue (the one place both the resolver and the LSP diagnostic read the shape rule from) and recording the consumer-to-producer linkage as a reference-gated {@link} the other way, from ScalarTypeCompletions in graphitron-lsp to ScalarTypeResolver.ParsedDirectiveValue, since the module dependency forbids the inbound direction. Claim 3 collapses an empty-slot fact stated five mutually incompatible ways (a record javadoc declaring emptiness legal, a facet path silently degrading it to a classifier rejection, two generation-time IllegalStateException guards asserting the opposite of the model doc, and two DSL.noCondition() fallbacks whose comments claimed to be "runtime-throwing" stubs surfacing at execution when noCondition() is a neutral no-op that would have silently run the query uncorrelated) into one enforcer: a producer audit confirmed both synthesis paths mint slots from the live catalog and a missing catalog routes through FkJoinResolution before any pair list exists, so On.ColumnPairs’ compact constructor now rejects the degenerate shape (columnPairs_emptySlots_rejectedAtConstruction), all five restatement sites went in one commit, and the eleven test fixtures that were the only real minters of empty slots got real column pairs; as a side effect the invariant converts `resolveFkColumnRefs’ silent column-drop on a partial catalog into a loud failure. Claim 4 sweeps the dead plan-file §-anchor families (the deleted error-channel spec’s §1/§2b/§2c/§3/§5 and the "Invariants §1/§2/§3" set) out of main-source comment regions, repointing each to its live enforcer (checkDeclaredCheckedExceptions, `ServiceCatalog.reflectTableMethod/reflectServiceMethod, ServiceDirectiveResolver.validateRootInvariants/validateRootListTableBoundReturnPair, TableMethodDirectiveResolver) or deleting the ordinal where surrounding prose already carried the fact, and renaming the rule7/rule8 locals to handlerCardinality/duplicateMatchCriteria; "Rule 7"/"Rule 8" survive in prose deliberately because ErrorChannelWalkerError.ChannelRuleViolation.ruleNumber renders the ordinal into diagnostics, so the names are anchored. External-spec cites stay (JLS §12.4.1/§14.4.2, the LSP-protocol §2.1.1 in Positions) and the R496/R521-owned sites are deferred by charter. Claim 5 retires the "nothing machine-enforces this clause" admission on Source.OnlyChild with ArrivalUniformEmitPinTest, a guarded census (vacuity floor plus an anti-over-deletion carve-out pinning that ChildField#source still mints both arms) asserting zero OnlyChild tokens across the generators package’s code regions via the guard family’s lexer, with the invert-or-retire hand-off recorded in R471’s body; the row-correctness constraint under query-alias fan-out stays as the strategy-agnostic forward burden. One rework round: the sweep’s grep had covered six module roots but not the full graphitron* glob, leaving two dead §4 cites in graphitron-sakila-service’s `FilmLookupNotFoundException, repointed in prose to FieldBuilder.checkDeclaredCheckedExceptions (a {@link} cannot resolve from that module). Independent-session In Review → Done review across both rounds; full reactor green under -Plocal-db with the {@link} reference gate and RoadmapReferenceGuardTest active.

  • R519 (Phase A 8c32c37`+7771d6e, Phase B `7eef0dd, In Review 67df32a; Spec d856b77, Ready d55cdf9, In Progress 67a8308): Retired @table on INPUT_OBJECT and deleted the GraphitronType.TableInputType bridge, the last consumer of the pre-R97 "an input type is itself a modeled relation" framing. With consumer-derived resolution complete (R97 Phase 2 + 2b, R457/R515), the directive drove nothing: an input’s fields resolve against each consuming field’s table, so the declared table was redundant metadata that could also silently miscompile a shared input reused across two tables. Retired-location convention rather than a parse error (the @notGenerated / @multitableReference / retired-@lookupKey-on-INPUT_FIELD_DEFINITION precedent): the SDL declaration keeps INPUT_OBJECT so graphql-java does not fail with a locationless grammar error, and TypeBuilder.buildInputType rejects any application at classify time with a migration message carrying the per-verb guidance the retired emitTableOnInputDeprecationWarnings used to give (drop the directive for filters and lookup args; @mutation(table:) on the field for DELETE and encoded-ID / scalar returns; return-derivation for INSERT / UPDATE), so the guidance keeps a stable diagnostic surface and stays pinnable at the pipeline tier. Shipped additive-then-cutover so every commit stayed green with the bridge available as a diff-free comparison point: Phase A migrated all 40 sakila-example input declarations and ~150 inline test SDL declarations across 28 files onto the plain path, then Phase B cut over in one exhaustive-switch cascade. Write-target rung 3 retires on classifier and grounder in lockstep, the ladders collapsing to @mutation(table:)-only for DELETE and return-derived > @mutation(table:) for INSERT / UPDATE; MutationInputResolver.singleTableInputType was deleted deliberately (it read the raw directive, so no compile error pointed at it, and leaving it would have let the grounder honor a bridge the classifier rejects). The rung-1-vs-rung-2 must-agree cross-check survives and re-pins over the two remaining rungs, while requireDmlDataTableMatchesInputTable goes as structurally tautological once rung 1 drives. The query-side TableInputArg-from-the-input’s-own-table arm retires (the carrier itself survives, still built field-relatively), and DmlWalkerInputArgResolution collapses to RawArg | Rejected with the stale “@mutation` fields only accept @table input arguments" message rewritten. The InputBeanResolver jOOQ-record-@service-param arm retires without re-sourcing, reversing an R97 seam note: it read only type identity as a proxy for "carries @table”, the conjunction is unauthorable once the directive is rejected, and re-sourcing it from the consuming field’s write-target fact would have rebuilt a global type-level aggregate of the exact shape R97 deleted. LSP/MCP cascade drops `TypeClassification.TableInput and its arms in TypeContext, LspClassificationLabels, DeclarationHovers, InferredDirectiveArgs, SchemaView, and EdgeProducer; the type-declaration hover survives with more honest content through PojoInput.resolvedTables (per-consumer tables, not one declared one), while input-field declarations lose their own hover / goto / inlay coordinates by design, and R337 was re-scoped from redirect tombstone to a live Backlog item owning the per-coordinate re-surfacing residual so the drop has an enforced owner rather than a prose handoff. Coverage: TableOnInputRejectionTest (pipeline tier, four cases pinning the per-verb migration text, the no-mutation-consumer filter case, and that the rejection is the type’s one verdict however many consumers) replaces TableOnInputDeprecationWarningTest; two new GraphitronSchemaBuilderTest pins are the regression home for the headline flip, one directiveless input consumed by fields on two tables resolving per-consumer, and a column present on only one consumer’s table rejecting just that consumer while naming that consumer’s table (previously: silent resolution against the input’s declared table, the cross-table miscompile). Docs swept across 24 pages; supported-schema-shapes.adoc regenerated without the leaf. Out of scope and carried by R520: the changelog migration note and dropping @table from the LSP’s INPUT_OBJECT completion list, still real because the location stays declared. Independent-session In Review → Done review, which additionally swept a stale GraphQLQueryTest comment citing four retired symbols; full reactor green under -Plocal-db including the PostgreSQL execution tier, the docs render, and the {@link} reference gate.

  • R535 (19696a3, reworks 0761194`+41d3ee0; Spec `306f233`+f0d030d, Ready `06371d8, In Progress a922982, In Review 1e2c4e5, rework requested 4e09c87`+fdbee5c): Removed the `@tableMethod directive outright, declaration and machinery both. R400 had withheld it from the v1 advertised surface but deliberately left it declared and behaviourally unchanged, which left a live gap: a consumer schema could adopt an unadvertised, undocumented directive and generate working code we would then owe support for. Full removal rather than R400’s REJECTED_ON_USE half-state, because the migration report’s "Legacy-only directives" section already renders any directive that legacy declares and the rewrite does not as removed-in-rewrite with a drop-before-migrating instruction, so deleting the declaration still leaves migrating consumers a clear message; a rewrite schema applying it now fails schema validation as an undeclared directive, the correct signal for something never advertised. legacy-directives.graphqls is untouched by design: its declaration is what routes the directive into the Legacy-only section. Gone with it: TableMethodDirectiveResolver, both GraphitronField leaves (QueryField.QueryTableMethodTableField, ChildField.TableMethodField), the DTO-parent batched arm in FieldBuilder, TableExpr.MethodCall (the seal reverts to Catalog | RoutineCall, and JoinPathEmitter.emitTableExpression collapses to one overload now that no node needs the call context, its MethodCall wiring-bug guard deleted rather than lifted), ProducerBinding.RootTableMethod, FieldClassification.TableMethod, both fetcher builders with their IMPLEMENTED_LEAVES rows, and buildTableMethodParentCorrelation. Two shapes the plan did not anticipate, both found by the compiler: FieldClassification.QueryTableMethod is renamed rather than deleted, because @routine reads and routine writes both project onto it and after removal it is the routine projection and nothing else, so it becomes RoutineBacked mirroring the sibling ServiceBacked (a user-visible rename, since the projection-record simple name reaches hover headers and inlay labels); and ServiceCatalog.reflectTableMethod stays under its directive-derived name because @condition reflects through it and it is the sole producer of the surviving MethodRef.StaticOnly, but with every surviving caller now @condition, its TableSlotPolicy enum, FORBIDDEN arm, expectedReturnClass parameter, strict ClassName.equals return check, and ReflectionError.ReturnContext are all unreachable and deleted, collapsing ReturnTypeMismatch to a single message form. LSP and MCP took structural deletions, not mention sweeps: the DirectivePolicy binding entry, the LspVocabulary Behavior pair, and the seal-arm rows in DeclTarget, LspClassificationLabels, DeclarationHovers, SchemaView, and EdgeProducer.EDGE_BEARING_FIELDS. The step-8 coverage meta-tests needed no edit: they derive their variant sets from the sealed hierarchy rather than hand-enumerating, so only the two main-source constants (IMPLEMENTED_LEAVES, EDGE_BEARING_FIELDS) were touched. Report policy drops tableMethod from WITHHELD_FROM_V1 (down to sourceRow, experimental_constructType); SchemaDirectiveRegistryTest’s declared-directive enumeration now pins the absence, so an accidental reintroduction fails a test. The migration guide gains one hand-authored replacement pointer next to the generated include, naming `@routine where the method wrapped a database function and @condition where it applied a filter. Fixtures and tests: the three sakila-example schema fixtures and their service methods, the execution tests, TableMethodFieldPipelineTest, both validation tests, TestTableMethodStub, the GraphitronSchemaBuilderTest enum rows, and the ClassifiedCorpus "table-method" example; the "record-method" example is edited rather than deleted (the @lookupKey half survives, and no coordinate is orphaned since Child/Fetch/List/Table/Record is demonstrated elsewhere). Roadmap dispositions shipped in the same commit: R529 Discarded (wholly about lifting the @tableMethod unsupportedPath runtime throw to build time, discharged outright), R240 renarrowed onto the surviving MethodRef.StaticOnly producers, R403 updated to record that reintroduction is now a fresh design rather than a re-advertising edit, and the residue swept from R333’s node definition, model-table rows, and resolution paragraphs. Two independent-session rework rounds, both on the same ArgCallEmitter javadoc block: the first because the removal orphaned the four-arg buildMethodBackedCallArgs overload (its only callers were the deleted TableExpr.MethodCall arm and the deleted fetcher builders) and the sweep’s find-and-replace had produced "neither @service nor @service`"; the second because the replacement text attributed one caller to a root service fetcher when both surviving callers are `ChildField arms (the ServiceTableField lift-back call and the ServiceRecordField rows-method body) and neither passes the null sourcesExpression the entry described. Independent-session In Review → Done review across all three rounds; full reactor green under -Plocal-db, 13/13 modules, with the {@link} reference gate and RoadmapReferenceGuardTest active.

  • R516 (cc270f1`+ffce130, reworks `b811bdb`+25d7e17`+2b64dc4`+696b939; In Progress `62f3d70, In Review 69004d7, rework requested 7bc3f2d`+f8d2668`+83318db`+9a36bf6, re-reviews `f377c2c`+27ca485`+08e6957): Narrowed the SourceKey.Wrap.TableRecord service-key contract to PK-only and reverted the full-row projection premise built up across R426/R436/R511 (R425’s underlying force-inclusion bug fix stays, re-scoped to the parent’s primary key). The mechanism was a narrowing, not a rebuild: TypeClassGenerator.collectRequiredProjection drops the reservedFullRow special case so Wrap.TableRecord falls into the same baseColumns arm as Wrap.Row/Wrap.Record (the RequiredProjection record itself retires, one axis being a List<ColumnRef> wearing a name), the __src_<col>__ reserved-alias scheme is deleted, and GeneratorUtils.buildKeyExtraction’s runtime `instanceof parent-shape fork collapses to one unconditional per-column field-identity copy onto a fresh key record (key.set(col, source.get(col))), correct on both arrival shapes because the PK is force-included under its base name on the SQL-projected side and a jOOQ-generated record always carries its own PK on the service-returned side. A Set<XRecord>/List<XRecord> Sources shape on a PK-less parent table now fails classification with a dedicated SourcesOnPkLessParent rejection naming the table (child coordinates only; root keeps its own arms), pinned at the pipeline tier with three controls. FilmService.titleTitlecase and CityService.cityUppercase were rewritten onto the idiomatic single-statement jOOQ batch fetch (selectFrom(..).where(PK.in(ids)).fetchMap(..)) as the manual’s canonical pair, and handle-services.adoc teaches the PK-only contract with them. Four independent rework rounds, every finding in the same defect class: prose describing the retired mechanism’s observable consequences as live, re-authored in three cases by the very edit correcting the previous instance (twelve surfaces at the first gate, six plus the token graduation at the second, the federation test’s failure-mode and wrap-attribution claims at the third, one false manual sentence at the fourth). The durable lessons shipped with the fixes: six identifier-shaped retired tokens graduated into RetiredVocabularyGuardTest’s registry, and the load-bearing jOOQ distinction was probe-verified rather than inferred, twice over (Record.get(Field)` on a field absent from the row type throws IllegalArgumentException while Record.into(Field...) yields null; a fresh typed record carries its complete row type with values unset, so reading an unset column off the key returns null and never throws, whatever the client selected). Test surface: ServiceParentTableRecordKeyExtractionTest and the federation test asserting the reverted behavior deleted, entities_tableRecordServiceChildOnly_serviceFetchedColumnResolvesNonNull added for the corrected contract under _entities dispatch, PkLessParentServiceSourcesRejectionTest added, DmlBulkMutationsExecutionTest re-anchored off the reserved alias, TypeSpecAssertions.appendsFullParentRow deleted and serviceChildKeyExtractionForksOnTypedRecord became serviceChildKeyExtractionIsUnconditional (retiring that body-scan family wholesale is R554). Independent-session In Review → Done review at a fifth gate, which re-verified the corrected manual claim against the generated fetchers and a live jOOQ probe; full reactor green under -Plocal-db, 13/13 modules, execution tier and docs render included.

  • R25 (f646a5d; Spec ba0e54b, Spec revision b27d7a9, Ready 2e5e66e, In Progress b9abcc1, In Review a570739): Measured the test pyramid: JaCoCo coverage instrumentation as an opt-in -Pcoverage profile in the parent pom (prepare-agent with append=false at initialize, report at verify, jacoco 0.8.15 in pluginManagement) plus a published report, so pyramid claims are quantified rather than asserted. The item builds the instrument and publishes its readings; it acts on nothing and adds no threshold or ratchet gate. Wiring: an unconditional empty <argLine/> root property (surefire’s late @{argLine} replacement needs a value in scope on every build), @{argLine} prepends in the three modules that hard-set a surefire argLine (graphitron-mcp, graphitron-lsp, graphitron-maven-plugin; javapoet’s now-redundant local property dropped), and a jacoco.outputDirectory property indirection repairing a falsified spec premise: the report mojo binds jacoco.destFile/jacoco.dataFile as user properties but gives outputDirectory no property expression, so without the indirection -Djacoco.outputDirectory was silently ignored and the per-tier report overwrote the combined one. Report surface: SourceCoverageReport (roadmap-tool, in-memory DuckDB over the per-module jacoco.csv globs, following LeafCoverageReport’s shape) renders `roadmap/source-coverage.adoc with four tables: per-module line/branch/method, graphitron per-package with per-tier line columns when tier CSVs exist, top-25 missed-lines classes with the cap stated in prose, and the leaf join carrying each sealed leaf’s classifier-trace count beside its class’s line coverage, the view neither report can produce alone. The page is never committed (a coverage percentage has no repo-verifiable fixed point, so no --verify drift mode either); render-adoc synthesizes a stub when it is absent and CI republishes the real page per trunk push. The tier vocabulary hoisted into TierVocabulary so LeafCoverageReport and SourceCoverageReport cannot disagree about the tier set, with the page prose stating that tier columns are slices that do not sum (cross-cutting classes fall into neither) and that doctrine-blessed renderer arm tests land in the unit column by design. CoverageAgentWiringCheck joins the roadmap-tool verify-phase check family, failing any build (not just -Pcoverage) on an <argLine> without a leading @{argLine} (drops the agent, false 0%), a failsafe execution without a distinct destFile, or a forkCount other than 1, since both wiring breaks are silent under coverage and invisible without it. CI: the build job gains -Pcoverage on every run (PRs continuously exercise the wiring, roughly 20-30 s agent overhead on a 139 s build) and flips verify to install so the two trunk-gated per-tier graphitron runs (-Dgroups=unit/pipeline, each with -Dleaf-coverage.skip to keep the inference-axis traces full-suite) resolve reactor artifacts; source-coverage regen/upload/download mirror the leaf-coverage chain. testing.adoc gains a Coverage measurement section carrying the recipes and both measurement blind spots (generator code in the Maven JVM is never instrumented; generator code in another module’s test JVM is collected then discarded at report time). First instrumented reading against the 2026-07-14 ad-hoc baseline recorded in the landing commit; R560 filed for un-skipping javapoet’s 400 tests. Independent-session In Review → Done review; full reactor green under -Plocal-db, coverage path re-exercised end to end at the gate (roadmap-tool exec + CSV + rendered page matching the landing commit’s recorded figures).

  • R549 (slices: 7d87b05 1, 7488765 2, 0e7fcd2 3.1, 334098d 3.2, bd1c712 3.3, ed9699d`+3febf31`+c0a7264 3b, e952960 4, 8a13616`+b41fcdb`+59b8738 5a, 4d0792b`+6f589ba` 5b, e4ea3bf 5c, 26fa34e 5d, 765e643 5e, c26966c 5f, efafa5d 6, 761ee16`+4906a0a`+65458b9 7a, bcd463f 7b, dfb06d6 8; Spec 648563b, Ready 88f2112, In Progress b9f5b9f, In Review bac46e8): The facts-and-commands programme: grain-first hierarchies and the three command relations, all eight slices landed serially 2026-07-28 to 2026-07-31. The reframing shipped whole: there was no intermediary command model to design, the sealed hierarchies already were the commands, and the work was labelling, re-homing, and grain repair. What now exists: the command / plan / render package triangle with PackageImportDirectionTest enforcing it, EmitPlan as the core step producing the global, type-keyed, coordinate-keyed launcher, projection, condition-consuming, fetcher-edge and connection-synthesis relations, one $project method per projection unit (nesting types promoted to anchor-prefixed units), every contribution gated on client selection (over-projection ended, ParentProjectionContainmentCheck and the required-projection walk deleted, the one deliberate SQL re-baseline), the fact-visitor engine with the pagination beachhead, the DML reentry fold retiring MethodCommandRegistry, the recompile graph as a typed projection over the plan (CompileDependencyGraphBuilder, 782 lines, deleted; the builder both over- and under-approximated, two real missing-edge classes closed), connection synthesis as a relation (the connectionName: plus @asFacet rejection dissolved as the user-visible win), the exemption-obligation registry with eleven closures, and the corpus asserting each coordinate’s launcher commitment via @commits(source:, result:) with all 15 launcher arms covered, zero exemptions. Instruments and ratchets installed at measured values and held: CommandSeamRatchetTest (entry points 24 to 18, generator leaf instanceof 104 to 71, case 89 to 76), ModelEmitVocabularyGuardTest (29 offenders, shrink-only), HierarchyKindRegistryTest (68 labelled hierarchies), 32 exact-SQL pins across three baselines byte-identical through every cutover except the one declared re-baseline. Measured 2026-07-31: generator branches 1,641 to 1,255, generator LOC 29,837 to 23,383, TypeFetcherGenerator 7,102 to 5,998 lines, methodgraph package retired; honest counterpoints recorded (FieldBuilder grew to 7,879, top-five concentration rose to 50 percent as the denominator shrank faster than the head). Follow-ups filed en route: R554 scope-extended, R556, R557, R559, R562. Independent-session In Review → Done review: full reactor green under -Plocal-db (3050 graphitron module tests, 704 sakila-example execution tests), ratchet constants verified at the spec’s claimed values, retirement sweep run against the item’s declared table with residue scrubbed at the gate ($project vocabulary in five sakila-example test files' prose, a stale PROJECTED_LEAVES comment in GraphitronSchemaValidator, two CarrierRewrite test names), and the renderer arm tests' rendered-string assertion mechanics recorded on R554 rather than held against the gate, the tree’s practiced convention with the retirement item already filed.

  • R563 slice 3 (the keystone, landed 2026-08-02): the leaf-member coexistence window OPENS. Per-trigger fact visitors (condition, orderBy, lookup, service, write) join the pagination slot in GatheredFacts; the minted OperationMemberRelation (post-walk trigger-fact fold) becomes the production behind GraphitronSchema.operationMembersOf over the flat classified index; the leaf-derived projection survives the window only as the membership-agreement pin’s comparison side (OperationMemberMintPinTest, canonical record-component rendering over the corpus plus per-kind coverage fixtures), the walk-less-schema fallback, and the leaf-local derivation behind the two re-fetch predicates; Operation re-labelled COMMAND → RESOLVED_VIEW (derived summary column); `LauncherRelation’s single-operation javadoc claim retired for the host-relationship statement, enforced by the new reentry-launcher agreement pin. The window closes with the last 6x dissolution slice, whose landing commit appends the closing line.

  • R570 (38553f5): ExternalCodeReference.name and the <namedReferences> Mojo parameter removed outright, no transitional shim. The argument was a short alias resolved through Maven config, but the element it was named for had already been renamed from the legacy generator’s <externalReferences> without the migration recipe saying so, and the three readers disagreed about an unresolved name: @service / @externalField rejected via ExternalRef.lookupError, path-step @condition blamed a missing method, and field / argument / input-field @condition returned null, which ConditionResolver reads as "directive absent" and which silently dropped the predicate from the generated SQL at exit code 0. Dropping name from input ExternalCodeReference in directives.graphqls makes the site a schema-load failure ("Fields ['name'] not present in type 'ExternalCodeReference'") and takes every reader arm, ExternalRef.lookupError and its two resolver consumers, the RewriteContext and CompletionData components, NamedReferenceBinding plus toNamedReferenceMap, and the LSP’s validateLegacyNameLeaves diagnostic and name-to-className quick-fix with it; ARG_NAME stays, being also @field(name:) and @table(name:). SdlActions keeps the generic action framework with an empty registry, and CodeActionsTest moved into the package to drive the three activation points and the message branches through a new package-private CodeActions.compute seam with a test-local action targeting a live deprecation marker, so the bulk-migration machinery keeps its coverage between registered migrations. Docs: the deprecations index gained a Removed table, and migrating-from-legacy.adoc files the argument under hard removals as step 1, since the schema does not load until it lands. Independent-session In Review → Done review: full reactor green under -Plocal-db, retirement sweep clean (every surviving namedReferences mention is deliberate migration prose naming what is gone), no code-string assertions in the delivered tests. Two findings filed as follow-ups rather than held at the gate: R578 (the generic descendUnknownArgs diagnostic the LSP now falls back on reports Warning for what is a hard build failure, inverting the editor/build strictness relationship) and R579 (FieldBuilder.parseExternalRef’s `parentTypeName parameter went unused when the deprecation warning came out). Process note recorded at the gate: the item transitioned Backlog → In Review in the implementation commit, so it never took a Spec → Ready sign-off, and both design forks the Backlog draft had recorded were reversed by the implementer without independent review; the reversals were found defensible on the merits at this gate, but the missing transition is the reason they had to be re-derived here.

  • R563 slice 6c (the last dissolution slice, landed 2026-08-03): the leaf-member coexistence window CLOSES. The read-family dissolution is complete: the lookup triplet (6a), the DML verb split (6b) and the routine read (6c) folded onto their grain siblings with the operation content carried as member payload, the pivot pair’s aggregate parameters single-homed on the Pivot member, and the surviving leaves hold the reconstruction key leaf = f(source, delivery, target) under the new derived-table enforcer (LeafReconstructionKeyTest, one tolerated collision: the single/bulk DML carriers, an input-cardinality exception with a named owner). The leaf ratchet stands at 10/8/22/4 from the 12/15/24/4 opening baseline; emit byte-identical through every dissolution slice.

  • R571 (c6c7024, rework 067d749; Backlog → In Review 77e8adc, approval reverted 2178df3`+bf3d987, In Review `ffece00): LspTrace, a default-off span seam in a new no.sikt.graphitron.lsp.trace package attributing LSP wall-clock to a named phase, so an editor session that stops responding on a large subgraph produces an attributable log instead of a symptom report. Instrumentation only; it fixes none of the five ranked mechanisms it exists to discriminate between (inline diagnostics on the message-reading thread, whole-workspace recalculation per build swap, per-site catalog rescans, full-buffer decode plus type-index walk per edit, uncancelled superseded requests), and each of those is its own item. Three design decisions carry it. Open and close are separate lines, so a phase that never returns emits an unmatched >: a duration-only format shows nothing at all for the hang the seam exists to diagnose. Output bypasses slf4j for System.err or a file named by graphitron.lsp.trace.file, because the stdio launcher speaks JSON-RPC on System.out where a console appender would write, and because that same deployment usually carries slf4j-api with no backend bound, so a logger-based seam would emit nothing; the sink is captured at class initialisation so a later System.setErr cannot redirect trace lines onto the protocol stream, and sinkForTesting stays package-private for the same reason. Off is genuinely free: span() returns a shared no-op singleton, pinned by instance identity rather than by absence of output, with int/long detail overloads so a count on a per-keystroke path does not box at the call site either. Instrumented for discrimination rather than uniform coverage: the document-service notifications and four request handlers, the diagnostic drain with queued-file and per-file counts, the withView / withAllViews lock-held regions scoped to the lock so the duration reads as lock-wait, the mutate/notify split in enqueueAndNotify, the per-edit reparse and type-index refresh, the diagnostics document walk and its whole-report validator projection separately, and LspVocabulary.load. First measurement already corrected a ranked hypothesis: the dominant per-edit cost is the full-tree TypeNames walk at 13.8 ms, not the whole-buffer decode handed to tree-sitter, whose incremental reparse is 1.1 ms. The rework pass, requested at a first Done gate whose approval was reverted in favour of folding the findings back rather than routing them to fresh Backlog items, made the instrument worth keeping permanently: a time-of-day stamp on every line plus a one-off header carrying date, resolved threshold and pid, so an unmatched > can be lined up against the moment the editor froze and a file read days later describes its own provenance; $/setTrace mapped onto setEnabled so an editor can start tracing mid-session rather than relaunching and losing the state that provoked the problem, honoured in both directions there but enable-only at the initialize handshake, since most clients send trace: off as boilerplate and honouring it would silence a deliberately-set graphitron.lsp.trace before a single phase had been traced; and package-private slowMsForTesting, openSink and enabledFrom seams making the SLOW tag, the file sink with its fallback-to-stderr arm, and the GRAPHITRON_LSP_TRACE arm assertable at all, each previously resolved once into a static final at class initialisation and reachable by no test. Two forks decided as rejections with the reasoning recorded rather than left silent: routing output through window/logMessage would carry the diagnosis over the very connection whose framing and liveness are under suspicion, serialised behind every response and emitted from inside the workspace lock, and mainstream clients already surface a server’s stderr in an output panel; and writes stay synchronous because what reaches the sink is what happened right up to a kill, which is how a hang is usually resolved, so an async drain would lose the tail exactly when the tail is the evidence, and deferring the open line is incompatible with the unmatched-> design outright. The blocking-write hazard that argues for buffering is real at the two lock-held sites, and the how-to now recommends the file sink for hang investigation with that mechanism spelled out rather than listing it as an alternative. Coverage: LspTraceTest 7 to 14, GraphitronLanguageServerTest 3 to 6; dev-loop-internals.adoc gained the file-sink recommendation, the $/setTrace asymmetry, and the subtraction that gets at per-directive cost (diagnostics.compute minus diagnostics.validatorReport, over directives=) in place of a per-directive span that would bury the log. Independent-session In Review → Done review: full reactor green under -Plocal-db, 13/13 modules, 467 graphitron-lsp tests; no code-string assertions on generated method bodies; user-facing-doc check clean and retirement sweep not applicable. One observation filed as neither defect nor blocker: setEnabled is a JVM-global flip and graphitron:dev can serve several connections in one JVM, so one client’s $/setTrace off silences tracing for all of them, an unguarded door into the state the handshake asymmetry was built to protect.

  • R563 slice 7 (the corpus voice, landed 2026-08-04): the Operation seal RETIRES. @classified(operation:) re-grained to the required operations: [Member!]! member-list assertion (sorted arm-token multiset, the @commits grammar applied to the member relation; produced side read through GraphitronSchema.operationMembersOf), landed additively with both voices coexisting on all 97 corpus coordinates and a content-preservation bridge holding the retiring token equal to a fold of the declared list and target shape over declarations alone, then the single-token voice, DimensionTuple.summaryArmOf with its documented Nest/Fetch empty-set fiction, the SDL Operation enum, and the 17-arm Operation seal itself deleted (zero code readers remained; the corpus vocabulary is the OperationMember seal through the prelude Member mirror). MEMBER_ARMS re-sources the coverage obligation (domain = the 21 sealed member leaves, covered = declared-and-agreeing rows, six re-keyed exemptions), the new polymorphic-filter example reaches Condition.OnParticipant with two rows (the multiset voice’s demonstration), each declared list is fenced production-independently against DECLARED_SHAPES, the coordinate-grain axis census dropped its operation column for the member-grain census which gained the by-target-wrapper pair, and slice 5’s parked delivery-argument question is decided no with the residual gap recorded as fact in the slice record.

  • R563 slice 8 (obligation re-typing, landed 2026-08-04): the programme’s LAST slice. The end-state audit found every compile-checked obligation half already re-sourced by the dissolution slices themselves (live-computed sealedLeaves domains, @ProjectionFor pins matching the surviving leaves, total default-free catalog switches); the residue closed here was the un-compiled halves: LookupResolution.isKeyed() landed as the seal’s total per-arm accessor replacing eight scattered instanceof Keyed spellings (five presence-only sites migrated; a new arm now forces one decision instead of silently answering "not keyed" everywhere), the FieldClassification record javadocs dropped their hand-written leaf censuses outright (the total projection switch and the @ProjectionFor pins are the two compile-checked homes; the class javadoc now states the rule so the census does not regrow) and the false "label switch dispatches over the full generator-side permit set" claims were corrected in both projection files, the dual-arm dispatch pin re-typed from simple-name strings to class literals, assertHonoured intersects covered with domain once for honest counts on every row, the emptied stub chain lost its would-NPE stubbedError helper while the gate stays armed, and UPSERT_RETIRED’s reason stopped naming the deleted UPSERT leaf. Bound: the member-read half of the projection obligation is satisfied per axis (membership reads the relation, payload reads the member row the leaf carries by identity, the routine fork is source-axis grain outside the domain); no retired-vocabulary registry entries for the eight dissolved leaf names (zero recurrences found across every guarded habitat); `Exemption.HarnessSingleCatalog and the arm-population census filed as R586; the catalog’s delivery spelling recorded as belonging to a future delivery-voice item. Leaf ratchet closes at 10/8/22/4 from the 12/15/24/4 opening baseline; emit byte-identical through the slice.

  • R563 rework (gate review 2026-08-04, rework landed same day): the In Review → Done gate requested rework on the documentation tail, falsifying slice 8’s zero-survivals audit claim in two habitats. Closed in one pass: the migration fragment regenerated from a full-reactor trace (the four shapes slice 6c wrongly flipped to (not yet supported) restored, MutationServiceTableField among them; the missing --verify gate that let a partial-trace regeneration commit silently filed as R590), code-generation-triggers.adoc swept onto the keyed-lookup-arm vocabulary (the lookup rows across five tables plus the TableTargetField census, now 4 variants, with the lookup emission pointed at render/LookupRows; two directive-effect rows beyond the review’s enumeration caught in the same sweep), the four LookupValuesJoinEmitter test-prose sites re-pointed at render/LookupRows including the dangling test-source {@link}, and the requireNoGeneratedFilterOnLookup retired-vocabulary row reworded as the re-grain it was (the method is live in ConditionCommands; what retired is its leaf-identity trigger). The review’s non-blocking payload-storage residual (condition, orderBy and paginate stay leaf-homed; member rows a view on those axes) filed as R591 per its recommendation.

  • R583 (a7c9e21; Spec → Ready 34ee0be, In Progress 08271b8, In Review c553838): Pinned the typeId axis of BuildContext.resolveTargetKeys’ name-first read on both jOOQ-record decode arms. Coverage only, no production change: the resolver already prefers the `NodeIndex by-name entry over the backing table’s KjerneJooqGenerator metadata, so a @nodeId(typeName:) target takes the named type’s reconciled @node answer, but only the keyColumns axis had a discriminating test. Reverting the read order broke exactly one case out of 3120 (NodeIdPipelineTest.InputCase.EXPLICIT_TYPENAME_TAKES_KEY_ORDER_FROM_THE_NAMED_NODE), so a typeId regression would have shipped silently, and a wrong prefix is not cosmetic: it becomes the emitted decodeValues(typeId, nodeId) argument, the encoder returns null on a mismatch, and the generated helper then rejects every well-formed client id at runtime on an input that builds green. The six pre-existing typeId assertions were blind because no fixture reachable from them paired a metadata-carrying table with a differing SDL typeId (film and email carry no __NODE_TYPE_ID, and film_actor’s metadata `typeId equals its type name). Three cases close it, all model-level assertions on the resolved CallSiteExtraction, none on generated bodies: NodeIdRecordInputBeanPipelineTest.sdlTypeIdOverridingMetadata_readsTheNamedNodesTypeId_notTheTablesMetadata (@node(typeId: "FA46") over film_actor) and .siblingNodeTypesOverOneTable_readTheNamedOnesTypeId (two @node types over one table, the member naming the second, which is the federation shape the by-name read exists to serve and the shape a table fact cannot answer), plus the same override on the RecordKeyDecode arm in JooqRecordServiceParamPipelineTest, so the second consumer does not rest on the first arm’s coverage. No new fixture table needed; film_actor’s existing metadata and composite key carried all three. Independent-session In Review → Done review: full reactor green under `-Plocal-db, 13/13 modules, and the discrimination criterion re-run at the gate rather than taken on report, with resolveTargetKeys reverted to metadata-first across the whole module: 4 failures of 3150, the pre-existing keyColumns pin plus exactly these three, confirming both that the new cases discriminate and that nothing else does. User-facing-doc check and retirement sweep not applicable.

  • R585 (eb27e57 + ee9f4af, rework bf4971b; Backlog → Spec 012203e, Spec → Ready efd3700, In Progress 79f2598, In Review → Ready 281cf33, body collapse cbf0586): InputFieldResolution.Unresolved carried (fieldName, lookupColumn, reason) where every sibling builder-step result carries a typed Rejection, and three fan-ins joined k input fields' failures into one sentence reported at the consuming field’s location, so the rejection got less structured as the schema got more broken and five broken input fields produced one squiggle in the wrong place. Slice 1 typed the carrier and preserved every message: Unresolved(fieldName, location, rejection) behind one private factory that stamps the failing field’s own location, six discarded .message() calls handed through intact, the retired @notGenerated / @lookupKey causes converged from five spellings onto directiveConflict so each cause has one identity carrying its directive name, the FK-target key mismatch onto the Rejection.deferred arm its own message asked for, buildInputFieldCondition’s `List<String> out-param onto a typed InputFieldConditionFailure accumulator, and the four causes whose upstream still reports prose (ParsedPath, ConditionDirective.argMappingError, ArgBindingMap’s two prose-only arms) through one named boundary wrap so "not yet typed" sits in one identifiable place. Slice 2 dissolved the folds: each failure mints one located `ValidationError at the input field carrying it via BuildContext.mintInputFieldFailures, the consuming coordinate keeps a single rejection stating the consequence rendered from InputFieldsResolution.Failed(inputTypeName, tableRef, mintedCount)’s typed facts, and dedup happens at the mint boundary rather than any reader’s drain (addDiagnostic` idempotent by value over a LinkedHashSet, which also deleted FieldBuilder’s hand-rolled `ctx.diagnostics().contains(...) guard). Because the per-field fact is built from the input field’s own facts, one input type consumed by five mutations collapses to one diagnostic when they resolve against the same table and keeps all five when the tables differ; value equality dedups exactly when the fact is the same, which is available only because the rejection is typed. Also: the two collectInputFieldRejections(...).getFirst() write-target paths stopped dropping every mirrored rejection after the first; DirectiveConflict.directives gained a stated contract (every listed directive is applied at the rejection’s own declaration, a remedy belongs in the prose) and the @asConnection-on-inline-TableField site stopped listing the absent splitQuery; the @reference column-miss hint moved to the path’s terminal table, where the column was actually looked for. Coverage: InputFieldFanInDiagnosticsTest (count-asserted throughout, since a cause gaining a second producer shows up only in a count) with a producer-partition table recording which of the reachable producers are typed and which stay Structural on purpose; eighteen assertions across four pipeline tests moved from the consuming field’s message to the cause’s new home; no code-string assertions on generated bodies. Out of scope and still standing: typing ParsedPath (its own item; this one wraps it at one boundary and deletes the wrap when it lands), the other three carriers R58TypedRejectionPipelineTest names, and a typed cascade arm for the consequence pending LSP related-information plumbing. Independent-session In Review → Done review across two gates. The first requested rework: a nested @condition failure was minted under the fold level’s input type rather than the type declaring the field, because one accumulator is threaded through the whole nesting recursion and reaches the outermost fold, so the coordinate named was one the schema does not have and one fact minted from two consumers was two unequal values, defeating the dedup the design makes load-bearing; plus three test javadoc paragraphs describing the retired mechanism as live. bf4971b carried the declaring type on InputFieldConditionFailure and swept the prose. Second gate: full reactor green under -Plocal-db, 14/14 modules, 3197 graphitron tests, with the fix re-verified independently at two and three nesting levels rather than taken on report; retirement sweep clean across sources and docs; user-facing-doc check not applicable (no docs/ changes). The DirectiveConflict.directives contract is pinned at one of eleven producer sites, which is a spot check rather than a contract; filed as R608 rather than widened here.

  • R582 (062c1d7; Spec → Ready bbca905, In Progress 2828bf9, In Review 354fb6f): AdocXrefAnchorCheck, a check-adoc-xrefs gate on cross-file xref:<file>.adoc#anchor references naming an anchor the target page does not publish. Asciidoctor never resolves a cross-document target at all (it only rewrites the extension), so a wrong anchor renders as a working link that drops the reader at the top of the right page and logs nothing at any severity; the same-file forms it does check report at INFO, which failIf severity=WARN does not catch. Four such links were live on the site, all the same mistake: the underscore id form Asciidoctor’s own documentation shows, against a site that renders kebab-case because docs/pom.xml sets idprefix empty and idseparator to -. The rule the gate enforces is that a cross-file anchored xref must target an explicit block anchor ([#id] or [[id]]), never an auto-generated heading id, which is what keeps the check from owning a copy of Asciidoctor’s id-generation algorithm (a second source of truth free to drift at every AsciidoctorJ upgrade) and what makes the referenced headings rename-safe. It cost eight one-line anchor additions across five pages and no change to any referencing site, since every one of those headings already slugged to exactly the id in use. Three design decisions carry it. It runs against target/staging rather than the source tree, inverting AdocMarkdownTableCheck’s deliberate `target/ skip, because staging is the tree Asciidoctor resolves against: the alternative is a mount table mirroring the staging steps that still could not resolve a roadmap plan page, which exists as .adoc only after rendering. It binds in the docs module’s base build after render-roadmap-adoc, not in the docs profile, so -P!docs is gated too. And it chooses its direction of wrongness twice over: an unresolvable target is counted and printed but never failed (a wrong path is a different failure that 404s on the first click, and failing on one would turn every quoted example in roadmap prose into a build break; those 9 live cases are R596), while detection keys on a same-line attrlist, under-reporting the corner where Asciidoctor splices a bare target and a bracketed one across a gap, with the limit asserted in a test rather than left to be discovered. Whether a quoted reference counts is not a question the collector answers for itself: InertSpans.maskInert holds the one definition of which span forms are inert, so a bare backtick span counts (it publishes a live link on a hand-authored page) and both inert forms do not; the same argument moved the verbatim-block walk out of InertSpans.scan into a shared BlockContext. Findings name both pages, map each staged path back to its authored source (three mappings, since staging is populated three ways) and list the anchors the target does publish, so the message is actionable without guessing at a generated id. docs/README.adoc gains the rule and has its errors-vs-warnings paragraph corrected, quoting the syntax as plus-delimited passthroughs since README.adoc is itself a staged, scanned page. Independent-session In Review → Done review: full reactor green under -Plocal-db, 14/14 modules; the gate re-verified at the gate rather than taken on report, by planting the underscore form and observing BUILD FAILURE under -P!docs with the kebab-case fix named in the message, and by re-reading the rendered HTML for the four repointed links, all eight added anchors published exactly once, reference.adoc’s natural-language `<<Schema-qualified keys>> still resolving, and this item’s own quoted syntax published as literal text. User-facing-doc check clean; retirement sweep not applicable.

  • R595 (99bf654 + 5787b75 + 14330ac + f489031 + d8de7aa, corrections ee587c7 + ffe31f7; five-slice plan dc29447, Spec → Ready b84572d, In Review d87ec18; the module, DDL, capture loads and agreement driver shipped before the item reopened at its first In Review gate): the graphitron-model fact store, completed across five slices after the reopen. Slice 1 fixed the reopen blocker: both production call sites handed capture the registry KeyNodeSynthesiser had already rewritten in place, so the federation walk macro was inert, the synthesized @key transcribed as authored and unlocated, and the authored-picture anti-join broke; AttributedRegistry now carries preSynthesisRegistry(), a readOnly() snapshot cut where the loading rewrites end, TagApplier/DescriptionNoteApplier sit above the cut by decision (their output is in the emitted schema the store owes a round trip of) with KeyNodeSynthesiser moved below them so the pipeline’s order states the split, and the federation anchor runs off one pipeline run pinning provenance rows and source locations. Slice 2 renamed catalog_/extension_ to sql_/jvm_ (families named for whose vocabulary a row is written in), java_name to jooq_name, extension_scalar_constant to jvm_scalar_type_field, every renamed relation’s comment stating its filters; the slice also surfaced and fixed the module-wiring defect where an incremental build’s stale target/classes copy shadowed a DDL edit at codegen. Slice 3 reshaped constraints as the catalog does: sql_constraint supertype discriminated by type plus sql_constraint_column/sql_primary_key/sql_referential_constraint, capture reading JooqCatalog directly instead of the CatalogFacts projection whose narrowings (primary-key split, candidate-key dedup, missing referenced-constraint name) were baked into the store; sql_column.ordinal comes from declaration-ordered Table.fields() rather than reflection order. Slice 4 widened the class census to the full compile classpath, jars included, fixing the reported bug where a jar-resident @scalarType constant generated fine and red-squiggled in the editor; jvm_method.descriptor is the real JVM descriptor, store_source records every entry stamped by content hash, and completion ranks reactor classes first (28,556 classes / 205,262 methods measured, insert brought from 23 s to 13 s by a per-relation bind batch). Slice 5 persisted the store as an H2 file under <build>/graphitron-model: store_stamp (DDL hash + generator version) discards and rebuilds on any mismatch so no migration ever exists, StoreRefresh keeps partitions whose source still hashes to what store_source recorded (retention enforced by seeding FactSink.claim, so capture walks exactly as it would cold), stamps are written after the flush so a killed run leaves nothing claiming completeness, readers get a copy-on-open snapshot, and a second process holding the file falls back to in-memory rather than being read as corruption; measured about a third off the sakila-example module build. Independent-session In Review → Done review: full reactor green under -Plocal-db, 14/14 modules; reviewer rule verified by session trailer on all nine commits; no code-string assertions on generated bodies in the delivered tests; user-facing-doc check not applicable (no docs/ changes in the slices); retirement sweep not applicable (no Retired vocabulary section). The spec’s carried-forward residuals (declined decodes leaving no semantic-stratum record, `captureFacts’ second catalog walk, retained-partition scan skip, nested-class filter, shadowed-duplicate quarantine) survive the file deletion as a Backlog item filed at the gate.

  • R572 (db96429; Spec review passes 4bda96a + 6b06c77 + 3414c49, Spec → Ready d5efd16, In Review 266b10a): graphitron knew the graphql-java and jOOQ versions at generation time and said nothing, so subgraphs drifted onto old lines with no moment at which anyone noticed. A build-time nudge closes that: when a consumer’s resolved version is a minor line behind the one graphitron itself was built against, one advisory per lagging dependency names the observed version, the current one, the coordinate to bump, and the rule id that silences it. A usability nudge, not a compatibility gate; the compiler is the correctness bar, nothing added can fail a build, and there is no separately maintained support floor, so the reference moves for free when graphitron upgrades and consumers start being nudged toward the new line with no further work. Four decisions carry it. The decision, the minor-line predicate and the message shaping live in core (DependencyVersionWarnings) while the Maven boundary’s whole job is AbstractRewriteMojo.decodeDependencyVersions, turning both artifact sets into (coordinate, version-string) pairs, because Artifact and MavenProject are external untyped input that must not cross into the generator; the predicate needs the version decomposed rather than ordered, which is plain-string work, so ComparableVersion is declined and the (major, minor) integer compare gets 3.9 versus 3.20 right where a lexical compare gets it backwards. The consumer-side scope filter is an allow-list of {compile, provided, system} rather than a deny-list of test, because ResolutionScope.COMPILE and TEST differ by two scopes and excluding test alone would leave a runtime-scoped coordinate observed under dev and silent under generate, the same project saying two different things depending on which goal ran. It rides the BuildWarning channel as two Source.CODEGEN lint rules with a null location, next to the <sessionState> advisories, so <lint><disabledRules> suppression, LSP replay and MCP projection come free with no new configuration axis, and separate ids (graphql-java-version-lag, jooq-version-lag) let a consumer held on an old jOOQ line accept that nudge and keep the other. And the reference version reaches the mojo through ${plugin} / PluginDescriptor.getArtifacts(), the plugin’s own resolved realm, needing no property promotion and no resource filtering, which the reactor has no precedent for either. Silence is the interesting output and every case is pinned: at the reference version, patch-only lag within the current minor, ahead of the reference, the coordinate absent from either side, and a version string that does not decompose; a single-segment pin reads as minor 0 rather than as undecodable. Coverage across three tiers plus the invoker harness at both ends, since the ${plugin} route is the one claim in the design with no precedent anywhere in the reactor and a negative-only IT would pass just as green with the feature entirely dead: DependencyVersionWarningsTest (17), DependencyVersionDecodeTest (8, the runtime row load-bearing), a fires-then-suppressed case in LintSuppressionPipelineTest, basic-generate asserting an up-to-date consumer and one carrying no graphql-java are both silent, and a new dependency-version-lag IT declaring jOOQ 3.19.24 directly and asserting the nudge fires. The four <sessionState>-flavoured Source.CODEGEN documentation sites are widened to what the axis actually partitions, a whole-build fact with no SDL coordinate folded in at report assembly, and docs/dependencies.adoc gains a "Staying current" policy paragraph carrying no version numbers. Independent-session In Review → Done review: full reactor green under -Plocal-db, 14/14 modules; reviewer rule verified by session trailer on both implementation commits; no code-string assertions on generated method bodies in the delivered tests; user-facing-doc check clean (no roadmap-internal vocabulary in the new docs/ prose) and retirement sweep not applicable. The reference route was verified at the gate rather than taken on report, by reading the emitted line out of the IT’s own build log with the 3.20.11 reference resolved off the live plugin realm. One coverage gap filed as R611 rather than held against the gate: the watched coordinate org.jooq:jooq is the open-source edition, so a commercial-edition consumer (org.jooq.pro*, org.jooq.trial*) resolves no watched coordinate and is never nudged, which the same docs page’s promise that "the build says so" does not qualify.

  • R611 (ae883c6; Backlog → Spec da4dff4, spec revision cdbf9bb, Spec → Ready 7794f24, Ready → In Progress 6423076, In Review 9d5da48): R572’s currency nudge watched exactly one jOOQ coordinate, org.jooq:jooq, which is the open-source edition, so a commercial-edition consumer resolved no watched coordinate at all and fell into the absent-coordinate silence case: never nudged, at any distance behind, with nothing in the build saying why. Sikt runs the commercial licence and the Oracle dialect the <devDatabase> block accepts is commercial-only, so that silence covered most of the population the advisory was built for, and docs/dependencies.adoc had shipped those consumers a promise that "the build says so" which the build did not keep. The fix rests on one fact settled by the requester and recorded as an assumption rather than defended by machinery: the editions are version-synchronised, shipping the same release line under different group ids, so an observed commercial version compares directly against the open-source reference with no mapping and no second number to maintain. Three changes land together because widening the predicate is what makes the observed side multi-valued, so the compiler admitted no smaller step. A WatchedDependency constant becomes a library rather than a coordinate, carrying its canonical group id plus the group-id prefixes its other editions ship under (org.jooq.pro, org.jooq.trial), with the artifact id pinned exactly; prefix rather than enumeration because org.jooq.pro tracks the current baseline JDK and the -java-<n> suffixes rotate with each bump, so an enumerated list would go stale into silence at exactly the moment a consumer moves their baseline, and artifact-id exactness is what keeps the prefix from swallowing jooq-codegen and its commercial twins. DependencyVersions.observed() becomes a Map<WatchedDependency, List<ObservedVersion>> where the new ObservedVersion record pairs the resolved coordinate with the resolved version, and AbstractRewriteMojo.observedVersionsOf appends every match rather than collapsing on first occurrence, because Maven mediates per coordinate and not per library, so the old putIfAbsent (whose javadoc claimed a coordinate appears once, the sentence this item falsified) would have made the surviving observation a function of artifact-set iteration order; the reference side keeps a bare version and moved to its own referenceVersionsOf, since only the consumer side is scoped and only it can carry several editions, and the two stopped sharing one scope-parameterised method. Selection stays in the interior in DependencyVersionWarnings.lowestLine: lowest release line wins because that is the one holding the consumer back, ties break on the coordinate string so the message text does not move between runs on an unchanged project, and MinorLine became Comparable so the ordering is stated once. One advisory per library however many editions lag. Decided during implementation rather than in the plan: an observation whose version does not decompose into a line is passed over rather than allowed to speak for the others, so the silence case is preserved per observation rather than per library. No new LintRule; the suppression id stays jooq-version-lag so a consumer who already silenced the nudge keeps it silenced across an edition switch, and LintRuleRegistryCoverageTest’s hard-listed `Source.CODEGEN set does not move. graphql-java has no edition split and is untouched. Coverage in two tiers: DependencyVersionDecodeTest (19) pins every commercial and trial group id observed as the same watched library carrying its own coordinate, jooq-codegen unmatched under all four group-id shapes, a group id merely sharing the org.jooq namespace unmatched, and both editions at once carried rather than one dropped; DependencyVersionWarningsTest (23) pins that the message names the resolved commercial coordinate and provably does not name org.jooq:jooq, that the lowest line wins under both input orders and when it is the commercial one (which is what stops the selection collapsing into "prefer open source"), the coordinate tie-break under both orders, one advisory per library, and an unreadable observation neither speaking for nor silencing the readable ones. The invoker tier genuinely cannot cover this and the item says so rather than papering over it: only the open-source edition is on Maven Central, every candidate commercial group id returns 404 there, and a hand-installed stub would pin nothing the decode tier does not already pin. Accepted limit, considered rather than missed: once jOOQ drops an older baseline distribution, a consumer on org.jooq.pro-java-<n> can be told to bump a coordinate that has no such version, because their fix is a JDK-baseline move; nothing defends against it, since the alternative is per-distribution knowledge of jOOQ’s support window, which is exactly the machinery the version-synchronisation assumption forbids. docs/dependencies.adoc’s "Staying current" section gained a paragraph saying every jOOQ edition counts, that the warning names the coordinate the build actually resolved, and that two editions at once are spoken about on the lower line. Independent-session In Review → Done review: full reactor green under `-Plocal-db, 14/14 modules, with DependencyVersionDecodeTest 19/19, DependencyVersionWarningsTest 23/23 and LintSuppressionPipelineTest 6/6; reviewer rule verified by session trailer on both implementation commits; no code-string assertions on generated method bodies; user-facing-doc check clean; retirement sweep clean (AbstractRewriteMojo.versionsOf and the WatchedDependency.coordinate() / groupId() / artifactId() accessors have zero live references, and the surviving putIfAbsent is the reference side, re-justified in place). The open-source end-to-end route was re-verified at the gate rather than taken on report, by reading the advisory out of the dependency-version-lag invoker’s own build log with its coordinate now produced from the live artifact. One residue noted and not held against the gate: the new docs paragraph enumerates org.jooq.pro, org.jooq.pro-java-<n> and org.jooq.trial but omits org.jooq.trial-java-<n>, which the code covers and DependencyVersionDecodeTest pins.

  • R618 (a82f59a the carrier end to end, b2b065f execution proof for both null-data outcomes, 726586a user documentation and coverage refresh, 2929b84 the fact-minted write membership; Spec → Ready c2c234d, Ready → In Progress 456ecc3, In Review 58e0949): a @routine write on Mutation could only return the terminus @table type through a @routine + @reference chain, and a hop-less @routine landed the single-node typed Deferred, so the return shape most authors reach for on a fallible write, the payload carrier, was blocked twice over and the author’s only route to a typed errors list was to abandon @routine and hand-write the call behind @service. Both other write families already admit that carrier; this item gives the routine write the DML carrier’s split exactly, and raises it to a stated rule: the write transaction contains the routine call and a projection of the routine’s own result columns, nothing else, ever, so the mutation fetcher owns step 1 alone and step 2 belongs to the payload data field the DML family already routes it through. The return shape classifies once and the path’s seat derives from it: a @table-bound return is the shipped chain path, untouched end to end, and a carrier return is the new MutationField.MutationRoutineWriteRecordField leaf whose one legal reference seat is the payload’s data field, so @routine + @reference with a carrier return rejects as a typed Rejection.directiveConflict naming the data field as the path’s seat rather than admitting a second spelling of one fact at two grains. The capture is two facts at two grains and the model says so: a hop out of an FK-less routine result is keyed by name-matching the target table’s primary-key columns against the routine’s result columns, and because the pairs' target side is the target PK by construction, the read-side correlation keeps reading a uniform total EmittedCarrierBinding.correlationColumns() on every carrier family while the pairs' source side, the routine-only fact step 1 projects, travels on the ProducerBinding.RoutineEmitted arm alone; the leaf’s compact constructor pins pairs non-empty, keying name-matched, and target side equal to the target PK, which is the no-join-in-the-transaction rule expressed in the type system. The derivation is one pure function (BuildContext.deriveRoutineCarrierPairs) with a single grounding site, so the two independent derivations that would have had to agree never exist; its typed failure surfaces on the mutation field with a candidate hint over the routine’s actual columns and no false condition: fix clause, since a condition join has no key tuple to capture. The third emitted-carrier arm is where the axis wanted reifying rather than extending, and only half of that reification landed: EmittedCarrierBinding over the three arms, taken, because the activeChannel gate in FieldBuilder.transportForParent was a hand-maintained boolean disjunction whose miss is silent (the carrier’s errors field would bind Transport.PayloadAccessor, the one transport a directiveless structural carrier cannot serve, while selectErrorsTransport never runs), so a classification-tier transport pin rides its own test rather than the data field’s; the consumer-side merge of the three memo maps, accessors and near-duplicate classify blocks declined and filed as roadmap/emitted-carrier-binding-consumer-consolidation.md, because the two real diagnostics have fixture-pinned wording and unifying them would drag the untouched DML and @service emit paths into this item’s acceptance surface. CarrierFamily.ROUTINE earns its arm on the ID axis (the ID-element permit is the DELETE PK echo and a routine write has no PK-echo shape, so it refuses the element outright at any wrapper) rather than on provenance, and RoutineDirectiveResolver split a node-only resolution out for the carrier seat with the chain path’s resolve behaviour-identical. Two null-data outcomes are pinned as distinct and only one goes through the channel: (a) the routine raised, the catch arm returns the non-null all-null-column sentinel so graphql-java traverses into errors instead of short-circuiting on a null parent, and (b) the routine succeeded and the committed row is invisible to the post-commit re-read under a row-level-security read policy, which is the motivating consumer’s happy path and renders data null with errors empty, no sentinel and no field error. Outcome (b) is what makes the data field’s nullability load-bearing, so a non-null single data field is an AuthorError at classify time naming the zero-row reason: non-null propagation would null the whole payload and destroy the errors list, which under the motivating RLS setting is the response’s entire informational content. The leaf ratchet rises 8 to 9 against a constant whose javadoc said the pins move only downward; the rise is source and target grain (a bare routine call vs the sibling’s chain, a payload record vs its post-commit terminus table) with the operation unchanged, which is the case LeafReconstructionKeyTest’s class javadoc contemplates, and the count-preserving fold was rejected because it would give one leaf two targets and make `leaf = f(source, delivery, target) untrue as a function; the class javadoc’s flat downward-only sentence was reworded to the surviving-grain rule rather than left contradicting the constant beneath it. Coverage: eleven classification cases in GraphitronSchemaBuilderTest (admitted with and without an errors field, the transport pin, list cardinality read at the data field’s seat, the fourth cell, the unmatched-PK keying, @reference on the data field landing the pointed Deferred, the record-element and ID-element refusals, D7, the narrowed single-node deferral) plus the RoutineBacked projection and a @classified corpus example; RoutineMutationWritePipelineTest’s step-1-only fingerprint as the mirror of the existing two-step pin, and both execution outcomes against real PostgreSQL, (a) as `Mutation.rentFilmPayload over the shipped rent_film FK-violation path and (b) as a genuine RLS fixture (secure_note plus a SECURITY DEFINER create_secure_note, in init.sql so the catalog resolves them, driven over a dedicated non-superuser role’s connection because superusers bypass RLS outright) with an owner-matching visibility control proving the null is the policy’s doing. docs/manual/reference/directives/routine.adoc gains the carrier as a second admitted return shape headlined by the one-sentence rule (with @reference the field returns the table the chain reaches, without it a payload whose data field declares its own path), and error-channel.adoc stopped claiming @service is the only upstream of an errors channel, which had been untrue since DML carriers started binding ErrorChannel.LocalContext. Deliberately out of scope, each landing as a typed Deferred pointing at its follow-up: the explicit data-field path declaration at any hop count (roadmap/routine-carrier-explicit-data-field-path.md, framed fact-base-first), @routine carriers on Query, and the non-table-valued routine kinds that stay in the narrowed deferral (roadmap/routine-write-result-shapes.md, whose author inherits the reworded deferral text both items share). Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db, 14/14 modules; reviewer rule verified by session trailer on all four implementation commits; no code-string assertions on generated method bodies beyond the call-site fingerprint carve-out the file’s own javadoc and SingleRecordPayloadPipelineTest already establish, with both fingerprinted claims independently proven at the execution tier; user-facing-doc check clean (no roadmap-internal vocabulary in either manual page, and the deferral messages name the question rather than a slug); retirement sweep not applicable. Two observations recorded rather than held against the gate: the GraphitronSchemaValidator arm is a documented no-op rather than a mirror of the classifier’s pins, which is the right answer at this seat (every pin is either a compact-constructor invariant or a classify-time rejection surfaced through validateUnclassifiedField, so a live arm would be unreachable code) and matches both the direct-return sibling and RecordCompositeField; and routineEmittedMemo’s first-producer-wins `putIfAbsent matches its two siblings exactly, so two hop-less routine fields sharing one carrier payload would carry the first field’s pairs, which fails as a generated-source compile error rather than a silent wrong key and is a pre-existing shape shared across all three arms rather than anything this item introduced.

  • R621 (legacy v9 line, c7684be80 + 463dd0c97 on branch graphitron-9-head; Spec → Done at user direction, the intermediate gates bypassed on explicit instruction, with the human user as reviewing party): split GeneratedExceptionToErrorMappingProvider under the JVM’s 65535-byte method limit in the v9 generator, unblocking the SIS subgraph whose constructor sat at 65,132 bytes. Both spec parts shipped. Part 1 deduplicates the per-operation lists keyed on the ordered mapping references per handler type, so operations sharing an error set share one declared List.of (a unique list keeps its operation-derived name; a shared one is named shared<Handler>List<n>). Part 2 splits emission into initMappings<n>() methods driven by a bytecode-size estimate (40 per mapping declaration, 16 per list declaration plus 12 per element, 24 per put; budget 30,000), with each list group (declaration plus all its puts) atomic so a shared list never separates from its registrations; constants verified against javap on a compiled replica (real costs ~8/element and ~16/put, so the estimates overshoot as intended). Two deliberate divergences from the spec body: mapping variables hoist to private fields rather than re-declaring per init method (each mapping constructed exactly once, and field access removes the >255-local wide penalty), and the split is conditional on the estimate, so under-budget schemas keep byte-identical v9.3.2 output and the defaultCase/noValidation golden files did not regenerate. The spec’s three coverage asks all landed: ProviderScaleTest generates a 500-operation schema, asserts multiple init methods appear under the default budget, and compiles the provider in-memory with the system javac (the module has no compile tier, so the test carries its own; javac is the enforcer of code too large, making compilation itself the assertion); ProviderChunkingTest pins the split mechanics under an injected minimal budget; sharedLists pins Part 1 independently of Part 2. Two adjacent defects rode along declared rather than silent: the multiple-error-fields registration bug is fixed in full (per-operation lists merge across error fields, removing both the duplicate declaration and the last-list-wins loss; multipleInOneResponse and bothGenericAndDatabaseInMultipleErrorsForOneResponse lost their illegal-code caveats), and the ExceptionToErrorMapping.equals-omits-handler pair the spec recorded as unreachable was shown reachable (the DATABASE handler’s className default collides with a GENERIC handler naming org.jooq.exception.DataAccessException when all other fields match) and is defused by keying the generator’s memo on (handler, mapping), pinned by equalMappingsAcrossHandlerTypes, verified to fail against the pre-fix generator. The constant-pool ceiling near 12,000 distinct mappings stands recorded and untouched. Open decision 1 remains open: 9.3.3 recommended over 9.4.0 (behaviourally identical output, no new plugin surface), release not yet cut. Full v9 codegen module green at 1,348 tests; example-server integration suite green end to end including the regenerated provider.

  • R624 (01d6dd4 the seam, 9c9d824 coverage across four tiers and the dot-path contract authored once, a7394c1 gate rework; Spec → Ready a9dda9c, Ready → In Progress c81464c, In Review 2322a09, rework requested e193389, rework In Review 58a1a73): argMapping looked like one authoring surface and behaved like four, so a dot-path that worked on @service was told on @routine that dot-paths do not exist, and the same string handed to the producer-binding probe went through a second, file-private parser that accepted entries the shared one rejects. The unification turns on a split the design states rather than assumes: the right-hand side of an entry names a GraphQL slot and walks input-object fields, and that namespace plus those walk rules are identical at every directive site, so ArgBindingMap.of (which already implemented them, liftsList and candidate hints included) becomes the only path from an authored string to a resolved binding; the left-hand side names a reflected Java parameter or a jOOQ IN parameter, discovered in opposite directions and at different times, so it deliberately does not unify and each site keeps its own vocabulary. parseArgMapping and of stay two separately callable functions because columnMapping shares the parser but resolves against catalog columns, and BuildContext.readConditionDirective parses at one point in the flow and resolves at another; fusing them would push both back toward hand-rolling, which is how the divergence started. Result gains a Failure sub-seal so the four sites that lift both arms with the same site prefix write one arm, while resolveConditionRef keeps the records apart and now carries the reason: its slot map is empty, so the shared message renders [] and the clause it adds to the UnknownArgRef arm alone is the only prose that explains it. @routine’s bypass goes: the single-segment guard and the `fieldArgumentNames membership check are deleted, argument-sourced parameters resolve against FieldBuilder.argSlotTypes, and both left-hand-side checks land in the same slice because routing removes the graphqlArg = param.name() side effect that was the only thing surfacing a left-side typo, and surfacing it only conditionally at that. The leaf gate calls ServiceCatalog.argExtraction, written but never called from the routine side, with a routine-specific rejection above it that closes the reported footgun: pParam: input used to pass validation and emit a cast that threw on the argument LinkedHashMap at request time. ParamSource.RoutineParamSource carries the routine narrowing structurally, deleting five unreachable emitter arms and their factory, and the emitter descends through ArgPathHelperRegistry, a per-host collector modelled on CompositeDecodeHelperRegistry whose helper takes the root as an Object parameter so the env and SelectedField forks can each supply their own. Documentation was the surprise the spec review turned up: the dot-path form had been shipping since the PathExpr work described in zero places, so the slice is first authoring rather than convergence, and service.adoc’s canonical `[[arg-mapping]] section is now the one description with six xref`s into it, plus a `diagnostics-glossary entry and a corrected handle-services.adoc claim this item falsified. Independent-session In Review → Done review, two cycles. The first pass accepted the seam whole and held on two findings: the new pipeline test asserted on generated method-body code strings, which development-principles.adoc bans at every tier and enforces at exactly that gate, and FieldBuilder.fieldArgumentNames had been left as dead code, package-private and static so javac stayed silent. The code-string finding cost nothing to close because the tiers the principle names as the replacement had already shipped in the same item, so the statement-form-not-ternary claim (implementation, not behaviour) was dropped and the helper’s shape is now read off the MethodSpec. Recorded rather than acted on: the ban is unevenly held in the tree, including in the test for the registry this one is modelled on, and relaxing it for registry-shape pins would be an argued edit to development-principles.adoc rather than a silent exception. Second pass verified both closures at trunk tip: full reactor green under mvn install -Plocal-db, zero body-string assertions net across the item, retirement sweep clean over all five declared terms. Retired: ParamSource.Arg#graphqlArgName, RecordBindingResolver#parseArgMapping, RoutineCallEmitter#nonRoutineParamSource, FieldBuilder#fieldArgumentNames, and the two @routine rejection strings. Deliberately left open, each filed: the LSP coordinate (R626), the coercing argument extractions (R625), the list-leaf cardinality gate (R627) and the producer-probe dot-path misgrounding (R628).

  • R569 (5b0614f the pilot’s substrate shadowed, 10f0711 the cutover, c41f244 the stratum’s DDL and loaders, 5abd97b the aggregate and the widened filters; Spec review passes 13ad425 + a3eb8b9 + c0a4e92, Spec → Ready 8526af5, Ready → In Progress d7e853c, In Review 18460ac): the diagnostics tool was entry-at-a-time only, so the first question an agent has on a schema mid-migration, "what is broken, in what proportion", could be answered only by handing over every entry; one measured session spent a subagent, 3.8M cache-read tokens and about six dollars paging roughly 700 diagnostics and hand-clustering them into a table whose three largest rows it then had to hedge, on dimensions that were all already present in the typed data the tool projected and discarded. The item’s answer leads with the model half rather than the tool: a violation is the validation phase’s derived fact and was the one product of that phase that never became one, so the store gains a diagnostics stratum and the aggregate is built as its first reader. Five arms sit behind one prefix-less diagnostic union view and nothing reads a base relation: the store-native pilot (intent_authored_claim_conflict, a derivation view with no writer), the rejection_ transcription residue and its ordered directives child, lint_finding in the linter’s vocabulary, build_warning_no_rule for the advisory arm, and the shipped javac_diagnostic. Per-vocabulary rather than one nullable bag, because severity is a function of the rejection’s kind for one arm, of the rule for another, warning by construction for the third and javac’s independent verdict for the fourth, and one relation holding any two would give one column two meanings; the advisory arm earns its own relation on a second asymmetry, suppressibility, since lint rows are post-suppression survivors keyed by rule id while advisory rows never met the filter. Each family is named for the vocabulary its rows are written in and carries its own retirement clock: rejection_ and walk_ have no referent once the sealed hierarchy and the walk are gone, while build_warning_’s comment states outright that it has no removal criterion, both its producers outliving the walk. The three model lifts an earlier Spec review had settled were reversed and dropped, recorded as a reversal rather than edited away: the sealed `Rejection hierarchy is retiring vocabulary, so a capability interface or a permit split invests in the surface being deleted, and each invariant is paid for in the view or the test tier instead (the actionable/deferred binary is a CASE over stored kind pinned by a one-row parity assertion, the lspCode() membership binds through a reflective walk that now compares both readers rather than a CodedRejection interface, the StubKey split is dropped clean with the absent arm stored as SQL NULL). The pilot is the drainage mechanism’s first flip and it landed in the order the design required: phase 1 reified the walk’s reach as walk_claim_domain_type / walk_claim_domain_field (two per-grain relations rather than the one the spec named, because a single relation needs a NULL field_name inside a primary key, which H2 refuses and which is the NULL-encodes-the-arm smell the spec rejects elsewhere) and landed the conflict view shadowed against the surviving Java reduction over the classified corpus; phase 2 flipped the report to project the view and re-aimed the DERIVED anchor at AuthoredClaimConflictsTest’s hand-written per-fixture messages, expectations the view does not produce, so the registration never collapsed into the view compared against a projection of itself. The reduction’s Java grouping, its domain-gate membership test and its per-grain queries are deleted; what remains is the decode of the view’s closed `verdict / directives vocabulary into the Rejection arms, with AuthoredClaim’s declaration order still owned by the enum. The tool half is faceted rather than a fixed report set: fifteen dimensions in a closed enum, each one column of the view, `groupBy as GROUP BY, the shared null-safe where as WHERE with IS NOT DISTINCT FROM so absence is a value both tools agree on, minCount as HAVING, and the tail as a second aggregate over the elided remainder, which is what makes a preset report literally a named (groupBy, where) tuple and collapses the drill-down filters and the aggregate into one mechanism instead of two that can drift. messageTemplate was measured and omitted per the Spec review’s stated rule, with the measurement recorded for the gate rather than left to be re-derived: 18 Structural rows over 53 corpus fixtures spanning 8 sentence shapes, a population the corpus over-represents by construction, against a permanent advisory population carrying no typed dimension at all; the dimension gloss says outright that advisory rows group by location only, and the partition pin declares two buckets so the prose bucket’s absence is pinned rather than implied. The invariant pins all assert on tool answers rather than engine internals, which is what makes a later per-family flip invisible to the suite: aggregate/drill-down parity per group, truncation honesty including a minCount past every group, the 200-group cardinality cap with balanced elision accounting, the null-keyed absent bucket read back as a drill-down, the canonical sorted directives render refusing to split a group on claim order, and the unknown-dimension refusal carrying the full vocabulary. Five shipped tests needed the substrate migration, one more than the spec named, and the cheap escape (keeping the report projection for handle-less callers) was closed by refusing instead: a handle-less call names the missing store handle rather than answering zero groups, because zero groups from a missing store reads identically to zero groups from a clean schema. graphitron-mcp gains its first store dependency, the handle reaching it as GraphitronMcpServer.StoreHandle from DevMojo’s one construction site, and the loaders home beside the report’s producer rather than at the workspace layer, on the reasoning R603 argued for `CompileFacts. The routing line and the manual’s tool row landed in the registration’s own commit, as the bidirectional instructions coverage pin requires. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db, 14/14 modules; reviewer rule verified by session trailer on all four implementation commits and both flips; no code-string assertions on generated method bodies in the delivered tests; user-facing-doc check clean (the manual row carries no roadmap-internal vocabulary) and retirement sweep not applicable, with the deleted shadow test and the dissolved reduction verified to leave no dangling prose. One spec claim was not met as written and is recorded rather than waved through: the spec asserts three times that the residue loader "reads the grain off ValidationError’s constructors" so that "no dot-split exists anywhere on this item’s path", and `RejectionFacts.coordinateOf does split the coordinate string, because ValidationError.forField takes an already-joined qualified name and giving it a two-part factory is precisely the carved-out sealed-Coordinate item; the invariant that actually carried the carve-out (one decode site, in the loader, replaced by a sealed switch when Coordinate lands, no column change) holds and is stated in the loader’s javadoc. Three improvements filed rather than held against the gate: the graph-ownership preamble now has two implementations because the helper was extracted for the new loaders instead of collapsing CompileFacts’ inline copy (R631), the drainage ledger’s migrated-family half is asserted only non-empty so a family flip forces no edit to it (R632), and `orderBy silently defaults on any unrecognised value while where’s `severity does not lowercase as its sugar twin does, both against the closed vocabulary’s own promise that a bad name fails loudly (R633).

  • R630 (d37e9b6 the pipeline overview rewritten, f1b5eea + 07ac443 the fact-model page and the first axiom, e75d9dc the forward voice and the reviewer taxonomy, d5d6c32 the generated schema reference, c7a7f1e the drift guard; Spec → Ready 32e9bd6, Ready → In Progress 83f793b, In Review 4ed3e54): the fact architecture had shipped and the documentation an agent orients from still described the architecture it dissolved, while the model’s why lived in a 2,200-line roadmap item that deletes on Done. Five slices give it a durable home, each a full vertical. pipeline-overview.adoc is rewritten to the shipped shape (capture transcribing totally into the base relations, the intent_ claim views and violation facts as the derived strata, planning joining facts into command rows, the render shell folding over committed commands, the unchanged writer contract, consumer compile), with a verb per stage and the classification walk named transitional throughout rather than quietly retired; the audited sibling pages were repointed, not rewritten. fact-model.adoc is the new why page, and its admission rule is what keeps it from becoming the next stale page: a claim moves out of the roadmap item only when it is true of the shipped store and can name the live test or gate that fails when it breaks, so target-state content stayed behind in an item whose status: tells the reader it is a plan. The migrated sections keep their headings with one-line pointer bodies, so sibling items citing them by name still resolve. The first axiom of development-principles.adoc is restated as "Decide once, at capture; carry the decision and its provenance as facts", paid for inside DocSizeBudgetTest’s word budget by displacing the sentences the new page absorbs, and its containment corollary now distinguishes the two enforcement grades it had been conflating: structural at capture, because a relation stores values and no live `Table<?> or Class<?> can cross into it, and convention on the walk. The forward voice was retooled with the same content so drafts get caught before they add to the migration’s debt: the principles-architect reading list gained both new pages and the reviewer taxonomy gained six named fact-discipline findings (leaf-where-a-fact-belongs, derivation-stored-where-a-view-belongs, provenance-flattened, private-model, emit-vocabulary-entering-the-model, keying-axis-confusion). The reference half is the decision that the what stays DDL-only: no hand-authored page mirrors the relation list, and one page per family plus an index is generated at build from the COMMENT ON text itself, so the reference cannot drift from the schema by construction. Structure is data too, in a new meta_ family whose rows are the schema describing itself: meta_family (the roster and each family’s charter, migrated out of the header prose that had already drifted twice, its "Ten families" count missing a stratum it described below), meta_prefixless_relation (the exemption polarity, so a new prefix-less relation fails the gate until a row argues it in), and meta_relation_family (the INFORMATION_SCHEMA census joined against both). Three deviations from the frame settled at Ready, each recorded rather than absorbed: the meta relations landed as views over row values rather than tables, which makes their rows constant per DDL hash by construction so StoreRefresh, the partition gate and the warm census need no exemption that could rot, and the uniqueness a PRIMARY KEY would have given is four bidirectional roster gates in FactSchemaGateTest instead; the accepted AsciiDoc subset came out narrower than "monospace, emphasis, lists", because a deliberate _pair_ and the accidental activation the gate exists to catch are mechanically indistinguishable, so CommentRenderabilityGateTest accepts plain prose plus paired monospace spans only and widens by deliberate edit; and slice 5 landed with slice 4 rather than standalone, both needing the same reader. That reader is the item’s load-bearing structural choice: StoreCatalog in graphitron-model is the one answer to "what relations exist", and both the renderer (SchemaReferencePages, homed in roadmap-tool so rendering code stays off the production classpath) and the drift guard read it rather than regexing the DDL, so two mechanisms of different fidelity can never answer the question differently. check-schema-identifiers closes the loop the old pipeline overview fell through: every backtick-quoted identifier in the authored architecture pages that starts on a family prefix must resolve as a family, a relation or a relation.column pair, and non-vacuity floors fail loudly on a moved habitat, a pageless tree or a store that parses to nothing rather than passing on an empty scan. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db, 14/14 modules, with both new build steps verified live and non-vacuous (13 pages rendered and site-rendered, the guard reporting 17 pages resolving against 129 relations in 12 families); reviewer rule verified by session trailer on all six implementation commits and the flip; no code-string assertions on generated method bodies in the delivered tests; user-facing-doc check clean, and the slice that touched development-principles.adoc in fact removed the two roadmap-id citations it carried; retirement sweep not applicable. Every enforcer the two new pages name was verified to resolve to a live symbol, which is the promise those pages make about themselves. Two improvements filed rather than held against the gate: the drift guard scopes by family prefix and so cannot see the one deliberately prefix-less relation, which the pages cite five times (R635), and the generated reference prints H2’s meaningless view-column nullability as if it were a constraint while gating the whole key/constraint line behind a primary key nothing gates (R636).

  • R612 (6b4d1e0 one recipe value over a sealed source carrier, 0293deb the rest of the config family and the item’s five enforcers; Spec revisions b1373e9 + 98b00c1 + 9281dd8, Spec → Ready f87acb9, Ready → In Progress 13f980a, In Review b1606f0): the Maven and pom configuration fact family. SchemaRecipe.Binding widens to a sealed Entry (pattern or literal source), so every run transcribes a recipe and a programmatic graph is as replayable as a build’s; store_graph_schema_input.pattern generalises to kind plus entry_value under a three-value CHECK with no rekey. The row encode and decode live together in StoredRecipe, landing the decode in production. SchemaSource seals the source into a file arm carrying a normalised Path and a named arm carrying a label, decided at mint by the producer that knows: SchemaInput.plain retires, RewriteSchemaLoader.load narrows to the file arm, SchemaProblemDiagnostic.normaliseLoaded and SdlFactCapture.regularFile are deleted, and capture’s stamp decision becomes a switch over the run’s attribution map whose two legitimate misses (the bundled directives resource, TagLinkSynthesiser’s sentinel) are named rather than absorbed. `SchemaRecipe.expand returns a sealed Expansion (Resolved / NoMatches / ScannerTrouble), dissolving SchemaInputExpander into the mojo-side decode plus the core expansion, with Resolved.currencyRelevantMatches() the one place the named-literal exclusion is decided. GraphIdentity narrows to (name, baseDir) and capture’s subject becomes FactCapture.SubjectConfig, so the family cannot accumulate nullable positional arguments across the five entry points. The rest of the family transcribes on the grain rules the spec settled at the gate: store_graph_output (the three coordinates together, no row for a validate-only run), store_graph_tenant_column, store_graph_supergraph, the two <lint> halves keyed by value and by ordinal respectively, and the <sessionState> alternation as store_graph_session_state plus its arm payloads, with the nested unmount alternation’s opt-out spelled as a missing disconnect row. <supergraph> documented in mojo-configuration.adoc. Tests: the five enforcers the item owed ; the round-trip anchor in both halves with an explicit non-vacuity guard, the end-to-end source-name rendering invariant, the tagged capture that is the tree’s only fixture putting the synthesiser’s sentinel in front of the stamp lookup, the supergraph peer-set gate, and the currency projection.

  • R645 (b074489; Spec c6b9757, Spec review ef6961d, Spec → Ready 0607a19, Ready → In Progress 9fc5666, In Review f2da82b): projected @reference and @externalField leaves are admitted under a plain-object NestingField, the gate that accounted for 39 of one downstream subgraph’s 72 deferred errors. The filing hypothesis (an emitter hole) was wrong in both directions: the emitter is already total for both leaves, because ProjectionCommands.mintNestedUnit mints a unit per anchor whose $project receives the anchor’s own table local and FetcherEmitter.bind reads the value back by __rk_<resultKey> alias without consulting the parent, so their correlation is per-parent by construction; but the blanket nested-depth gate had been standing in for per-variant validation that never ran below a nesting field, so widening alone would have shipped silent wrong SQL (an @externalField carrying a @reference path emitted the helper call with the join path dropped) and a generator crash (a NodeIdEncodeKeys reference carrier reaching contributionFor’s invariant throw). Both halves ship, validator first. `validateField’s body becomes `validateVariantSpecific, called from three sites (the top-level walk, the nested walk, validatePivotSpec’s slot walk) so no site validates a leaf less than another does; the two guards and the two ordering checks move in whole, closing a live hole where a nested `BatchedTableField keyed on an array column mis-batched silently. The nested walk threads the enclosing NestingField’s `TableBoundReturnType as an anchor rather than a nested flag, which is what keeps validateColumnBackedField’s parent-type gate from false-positiving every plain nested scalar, and `validateNestingField splits into a shape check plus the walk so the list-cardinality rejection also fires on a nesting type nested inside another one. Multi-parent stays out (a separate gate in compareNestedFieldsShape, cross-linked from R323), and the @externalField parent-table assignability hole found on the way is R646.

  • R646 (e0f3583; Spec 8acc81a, Spec revision a2e4a55, Spec → Ready fa48b0f, Ready → In Progress 47fddef, In Review ef92337, In Review → Ready aa8cbb1, Ready → In Progress 18c2a13, In Review 3bd27eb): @externalField helper parameters are checked against the parent table. reflectExternalField documented its sole parameter as "assignable from the parent’s jOOQ Table<?> class" but only checked that it was some jOOQ table, and the parentTableClass argument was never read, so a helper typed on another table classified clean and emitted <Helper>.<method>(table) into a $project unit typed on the parent’s generated class: an authoring mistake surfaced as a javac error inside a consumer’s generated sources with no line back to the SDL. The unread argument widens to the parent’s TableRef and is read as two ordered value comparisons behind the existing Table-subtype gate: table identity (findTableByClass on the parameter class, compared through TableRef.denotesSameTableAs, so non-catalog supertypes admit and multi-schema same-named tables stay distinct) then record type (a concrete X<R> must name the parent’s recordClass). No live jOOQ handle for the parent crosses into ServiceCatalog or ExternalFieldDirectiveResolver; both layers fail open with the graphitron-sakila-example release-17 compile as the backstop, so neither can produce a false rejection. Four ComputedFieldCase rows cover the mismatch, the widened Table<?> accept, and both sides of the record-type layer; FilmExtensions.titleByName(Table<?>) wired to Film.titleByName is the compilation-tier witness that the widened form still emits a well-typed $project body. Corpus-neutral. The @condition analogue on reflectTableMethod is filed separately as R647.

  • R649 (a896327 the phase split, with the gate’s javadoc sweep landing in this entry’s own commit; filed 1e8487e, spec body e746fb1, Backlog → Spec a14e800, Spec revisions 5c119e7 + 5661a52, Spec → Ready b4f2f6c, In Review with the implementation a896327, rework requested 7a27aa4): ServiceDirectiveResolver.resolve reflected the service method, and therefore bound its parameters, before it classified the coordinate’s return type, so a problem belonging to the coordinate surfaced as a problem with the author’s Java signature. On a record-backed parent, where the batch key is unavailable by construction, the surviving diagnostic told the author their parameter matched no GraphQL argument and advised adding one, on a field that declares none; the honest rejection sat two arms below in the same classifier, unreachable. The fix is a phase split rather than a reorder at the losing arm, because the ordering defect was structural: ServiceCatalog gains decodeServiceMethod (reflection to a ServiceSignature carrying no java.lang.reflect.Method, which makes the raw-reflection containment invariant structural rather than commented), reduceClaims (one sealed ParamRole per parameter, so candidacy is decided once as a carried value instead of by three predicates two phases must agree to spell identically), and bindServiceMethod (extraction and minting only). Between reduce and bind sits the resolver’s new classify phase, which decides every rejection expressible from the signature, the coordinate and the field’s SDL shape, in one stated order: field shape, then coordinate, then signature fit, then binding. The coordinate reaches the resolver as a sealed ParentContext (Root / TableParent / RecordParent) in place of a key-columns list whose emptiness doubled as the root signal, which is exactly what had made root sites and record-backed parents indistinguishable; ServiceCatalog.PkLessParent dissolves into TableParent with an empty PK. The preserved return-type regime is a derived axis with an exhaustive switch rather than a Root || RecordParent disjunction spelled at each read, so moving a coordinate between regimes is a one-arm change and a fourth coordinate is a compile error. SourcesOnPkLessParent, the Sources element-class check, the child-polymorphic deferral and the record-parent deferral all stop depending on parameter declaration order; two winners flip deliberately (a root Connection return now beats batch-at-root, and batch-at-root now beats the strict return-type mismatch) and both are pinned. Severity never changes: everything rejected before is rejected after, only which rejection surfaces changes. validateRootInvariants dissolves, its ParamSource.Sources arm retiring as dead code whose message text had already drifted from the live copy, and the four FieldBuilder arms the resolver now rejects outright become invariant throws naming the classify arm that owns the verdict. ServiceCoordinatePrecedenceTest pins one fixture per precedence pair at the pipeline tier (the catalog structurally cannot observe an ordering decided above it), 12 of its 16 cases failing against the previous order; ServiceCatalogTest re-anchors its 47 fused-entry-point call sites on decode and bind, its strict-comparison group moving to the resolver’s phase and the mismatch messages to their existing pipeline pins. R648’s Implementation and Tests bullets were rewritten against the post-split code as a deliverable of this item rather than left as advice. Independent-session In Review → Done review, two passes. The first pass verified the split whole (full reactor green under mvn install -Plocal-db, reviewer rule by session trailer, no code-string assertions on generated method bodies, the four invariant throws traced as genuinely unreachable, the errors-lift hoist confirmed side-effect free, the declared retirement sweep clean) and held on one finding: reflectServiceMethod was deleted with 16 citations outside its own file still naming it, 8 as {@link} and therefore dangling. The javadoc reference gate misses them because ServiceCatalog is package-private and the gate runs at javadoc’s default protected visibility, so the rot was silent, and three of the stale statements misattributed the very responsibility this item moved (TypeFetcherGenerator still placed the strict return-type check in reflectServiceMethod; InputBeanResolver and MethodRef still named it as MethodRef.Service’s producer). Closed at the gate on the user’s instruction rather than by a rework cycle. Retired: `ServiceCatalog.PkLessParent, ServiceCatalog.reflectServiceMethod, ServiceDirectiveResolver.validateRootInvariants, looksLikeSourcesShape, couldBeSourcesShape, and the drifted root-batch message text. Three residue items filed rather than held against the gate (R655): the record-parent arm’s second trigger is pinned on its Result half but not its Scalar half, the decoded inferBindingsByType overload dropped the org.jooq.Table eligibility filter its reflection-based sibling keeps, and `ServiceCatalogTest’s 41 method names still carry the retired entry point’s prefix.

  • R648 (a033cf1; filed 1e8487e, Backlog → Spec 92a40fdd, Spec revisions f800724 + 7b18bb3 + 5ca178f + 4346e23 + 183a551, Spec → Ready (deferred on R649) d310b1b, Ready → In Progress a087f1d, In Review with the follow-ups filed a71b047): a child @service on a class-backed parent can batch, keyed by the table its Sources element type names. The contract relaxes from "the keys carry the parent’s primary key" to "the keys carry the key columns": the key stops being an accident of the parent’s PK and becomes a named set of catalog columns the author pointed at, resolved through the catalog at classify time. Two producers qualify and they are the arms of the new ServiceKeySource beside the pre-existing table-parent row: the parent’s backing is the declared record (read off env.getSource()), or its backing class exposes exactly one zero-arg accessor returning one (read off what the accessor returns, null-guarded, via the new name-free ClassAccessorResolver.enumerateZeroArg). The coordinate’s answer is a value (ParentKeyResolution: Root / Available / Rejected) computed in the resolver’s classify phase R649 built, consumed by both the binder (as the key columns, retiring parentPkColumns) and the leaf (as the stored component ChildField.sourceShape() now derives from, closing the latent-wrong Table answer on both service leaves). Record-backed parents flip from the root return-type regime to the batched-child one, and the residue the flip would strand is closed rather than left: a child @service declaring no Sources parameter is rejected at classify time on both parent kinds, which makes both key components non-null by construction, retires LauncherCommands.serviceRecordRow’s no-Sources production guard and `ProjectionCommands’ null-sourceKey` branch, and moves the DTO-shaped-parameter verdict from binding to classify where it cannot be masked by declaration order. Five newly-reachable rejections are named (no producer, ambiguous accessor, list-cardinality accessor, PK-less key owner, anonymous wrap on a class-backed parent); GraphitronSchemaValidator states the batched-child invariant once for both leaves (a Sources parameter exists, and its columns are the key owner’s primary key). One defect the change made reachable shipped fixed with it: RowsMethodShape.strictPerKeyType resolved nested payload classes through ClassName.bestGuess, which keeps the binary $ form. Execution-tier pin: three parents over two distinct films observe one dispatch, a deduplicated key set, and sparse keys from a single query. Docs: handle-services.adoc gains the class-backed batching subsection and the contract line rewrite; result-types.adoc gains the @service-child decision row. Independent-session In Review → Done review: full reactor green, retirement sweep clean (the three no-lift statements survive, as the spec demanded), two residues filed as R665 rather than held (the missing pin-fires assertion, and the wrong-table typed-record wording in the no-producer diagnostic); follow-ups R656 (@sourceRow-declared keys for scalar-only parents) and R657 (list-cardinality fan-in) were filed at the In Review flip.

  • R656 (afe6c7b; filed at R648’s In Review flip a71b047, Backlog → Spec d4027ae, Spec revisions 06a793e, Spec → Ready 431bb12, In Review with the implementation f658e09): @sourceRow on a batched child @service declares the batch-key producer a scalar-only class-backed parent cannot infer. R648 left two producer routes (the parent is the Sources element record, or exposes exactly one zero-arg accessor returning one) and a rejection tail promising nothing else; this adds the declared third route as the accessor route’s static twin: the directive names a public static method taking the parent and returning the Sources element record itself, site-derived, so the @table child’s RowN contract is untouched. New ServiceKeySource.FromLifter arm carrying a StaticProducerRef (canonical-name strings, resolved at the classifier boundary because the emit seat has no input to recover a cast target from); return contract enforced by class identity against the element class, since the emit copies by jOOQ field identity and no downstream compile would catch a same-table class mismatch. @service now outranks @sourceRow at the child classify site ; a field carrying both used to route into SourceRowDirectiveResolver and silently drop the @service ; and the @splitQuery redundancy advisory follows onto that branch. The reflection preamble both directive sites share (class load, unique static method by name with did-you-mean candidates, single parameter assignable from the parent) extracts into LifterMethodResolver, keeping the rejection vocabulary single-sourced; the directive declaration travels as the reduced fact SourceRowDeclaration on ParentContext.RecordParent rather than threading the SDL node inward. The declaration wins totally over inference (giving the two-accessor ambiguity an exit needing no class edit), JooqRecordCarrier parents reject it as redundant, and every producer-route message surface names the third route, retiring "has no route today". Validator confirmed arm-agnostic, no LSP work needed. Pipeline tier: happy paths on both service leaves, override, tie-break, both-directives ordering pin, advisory, and eight rejection shapes; execution tier: three parents deduping to two keys observe one batched call via a batch-size-encoding fixture service. Docs: handle-services.adoc three-route rewrite with the lifter example, result-types.adoc decision-row replacement. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db, no code-string assertions on generated method bodies, retirement sweep clean on all four declared terms, user-facing-doc check clean; one stale arm count in ServiceKeySource.sourceShape() javadoc fixed in the approval commit under the spec’s own "fix the count" instruction.

  • R671 (f3c6753 + 52a4ca3; Spec → Ready on the fifth independent review 836e772, Ready → In Progress e15e1e4, In Review cee6a42): a shared class-backed value type produced by a @service (batched child or root) and read as a component of a record-backed parent was rejected by the multi-producer source-type check although both producers put the same Java object at env.getSource(), because producers reported placeholders (Plain(java.util.Map) off an unpeeled batch return, Plain(java.lang.Object) off a component read) that could never compare equal; there was no schema-side workaround, which is a blocker for the federation shape where another subgraph fixes the type via @shareable. DomainReturnType splits into Claim (the three existing arms) and NoClaim, and GraphitronSchemaBuilder.collectDomainReturnTypeConflicts compares claims only, with the exclusion carried by the grouping map’s key type and no-claim producers retained in the rejection’s participant list rendering as "makes no source-type claim". Every class-backed producer mints through one factory (DomainReturnType.claimForBacking, routed through RowsMethodShape.fromBinaryName so a nested class is spelled Outer.Nested once rather than diverging from ClassName.bestGuess’s `Outer$Nested), and the shared result-return rule (claimForResultReturn) answers by fact rather than by leaf: TableRecord where the ReturnTypeRef.ResultReturnType carries a resolved table (newly threaded from BuildContext.resolveReturnType), the backing-class Plain where only a class grounded, NoClaim where neither did. That rule also repairs the root @service twins' arm asymmetry: MutationServiceRecordField’s unconditional `TableRecord is confined to the population that resolved a table, and QueryField.QueryServiceRecordField answers identically on every fork, so one payload type no longer wears two arms depending on the operation root, while the DML-vs-carrier cross-arm tooth is pinned from both roots. The placeholder sweep migrates the polymorphic, pivot and errors-list answerers to NoClaim, leaving OutputField.OBJECT_CLASS only as peelToClassName’s structural fallback. Coverage is positive-assertion at every tier: seven pipeline cases pin the producers' equal claims (including the ungrounded-object-type sub-population of `ScalarReturnType, which the plan hedged as possibly unconstructible and which proved constructible), the cross-arm conflict cases keep firing, and the shared-value-type shape reaches graphitron-sakila-example’s release-17 compile for the first time. The masked-conflict sweep over the full reactor surfaced nothing. The compilation fixture flushed out a separate emit-side nested-class spelling defect, filed as R688 rather than absorbed. Independent-session In Review → Done review: full reactor green under `mvn install -Plocal-db, no code-string assertions on generated method bodies, retirement sweep clean on all three declared terms, user-facing-doc check clean; one bookkeeping note, the item body’s implementation notes cited pre-rebase SHAs, corrected to the pushed history here.

  • R650 (348f914 + 0c55288 + cd46fe9 + 3201386 the root half, 3d4c78a the child half, 5d256cd + 18fca6d the review rework; Ready → In Progress ce49fcd, In Review 43293b0, In Review → Ready 8963951 + 4d66c8c, In Review 76a4ad9): @asConnection is supported on a field returning a single-table discriminated table interface, at the root coordinate and the child coordinate both. The route is not the cardinality invariant the first spec signed off on. DiscriminatedTableFragments.assembly emitted two join families and only one of them, the joined-detail LEFT JOIN, is proven single-valued (TypeBuilder.resolveJoinedTableParticipant requires the detail’s FK columns to be its own primary key); the other, a cross-table participant scalar one @reference hop off the base, had nothing checking the hop’s target-side uniqueness, so a reverse-orientation hop onto a non-unique FK classified green and fanned out. Defending that join with a new targetAtMostOnePerSource invariant would have made the discriminated coordinates stricter than the plain table child on the same authoring surface, so instead the join goes: cross-table fields lower at capture (LauncherCommands.discriminatedBranches mints a SelectTerm.ScalarSubselect carrying the fixed participant alias and the branch’s discriminator gate) and render through the same PathFragments.scalarInnerSelect the plain scalar @reference arm calls, a correlated subquery capped .limit(1). The conversion is unconditional rather than pagination-scoped because assembly is shared, so the latent fan-out defect dissolves on the list, single, service and DML shapes in one commit, and the paginating statement is then sound by construction: base table, proven 1:0..1 detail joins, row-neutral select-list subselects. No schema newly rejects. The fragment splits on its fact boundary into projection (everything populating the field list) and joinedStep (the step declaration plus what is now the only join chain), because the page request must observe the populated list before dsl.select(...) is composed; assembly stays as the composition so its four existing call sites are untouched. Both paginating callers ride that seam, and BatchedRowsFragments.windowedPageTail extracts the plain batched child’s per-key windowing protocol (ROW_NUMBER partitioned by __idx__, pre-rank seek, the __rn__ <= limit outer filter, the cursor-independent count source) so the discriminated binder shares it rather than mirroring it, the plain arm’s emitted SQL staying byte-identical. Two defects the plan did not anticipate shipped with it: the type-conditioned selection gates were depth-blind (getSelectionSet().contains("<Type>.<field>") matches only at the top of the selection, so under a connection a participant’s detail column and cross-table field were silently dropped from the page), now offering the **/ form too, latent for every non-root coordinate; and joinedDetailAliasDeclarations’s javadoc had been dangling since the conversion renamed its `{@link} target, invisible to the reference gate because private members sit below its show level. The declared retirement of "gated/conditional LEFT JOIN" as the cross-table description took three passes and two review cycles, the first two sweeping a reviewer’s enumerated list rather than the declaration; the third re-ran the grep against the declaration and found five habitats no review had listed, the consequential ones being two user-manual pages teaching authors the wrong cardinality and performance model, and a FilmContent SDL description rendering a false mechanism claim into the generated schema. The phrase graduated into RetiredVocabularyGuardTest as a second PHRASE_REGISTRY beside the identifier one, matching the adjacency cross-table … join rather than a proximity window, because correct prose contrasting the two mechanisms sits inside any window and a guard needing an allowlist for good writing gets suppressed; both directions are pinned, and the guard gained .sql as a habitat, which failed the build immediately on an unrelated pre-existing hit. Coverage: the capture-tier lowering pin where the term is decided, two RootLauncherSqlBaselineTest cases (the page query and its totalCount twin counting the base under the discriminator restriction), a BatchedChildSqlBaselineTest windowed case beside its plain sibling, an execution-tier fan-out fixture pinning one entity per base row over a genuinely fanning hop, root page walks including backward and before-cursor and a detached base row surviving NULL-through under the seek, a child before-cursor walk through the reversed seek, and corpus coordinates at both ends putting DeliveryFactPinTest on gate duty. Retired: both deferral messages, the (DiscriminatedTable, Connection) constructor backstop half and its RootLauncherRenderer throw, TABLE_INTERFACE_ROOT_CONNECTION_DEFERRED, crossTableJoinChain, crossTableAliasDeclarations as a join concept, CrossTableField.aliasVarName(), a JoinStep javadoc paragraph, and targetAtMostOnePerSource, which never landed. Independent-session In Review → Done review, third cycle: full reactor green under mvn install -Plocal-db (5517 tests, 0 failures), zero code-string body assertions added across all seven commits, user-facing-doc check clean over five docs/ files, and the retirement sweep re-run independently against the declaration rather than the spec’s account of it, every surviving hit being joined-detail prose that legitimately keeps the phrase, the deliberate contrast sentence, or the guard’s own fixtures. The new guard was verified non-vacuous by injecting the retired phrase into three habitats and observing the build fail with the successor named. One bookkeeping note, not held: the item body and `18fca6d’s message say the phrase entry covers "eight of the seventeen habitats", but running the shipped pattern over the pre-fix tree matches 13, all in scanned regions, so the entry under-claims its own reach.

  • R642 (fb5ed1b + 1cf41a9 + 5de78d5 + 119e869 + b655ce3 + c2a14c2 + 8de07e2 + df5e7ff + bc245fe + 7fcfb86 the ten slices, dde4034 the self-review sweep, d99b7f3 the delivery record; Backlog → Spec bd489b2, Spec revisions 663adb2 + 6317a11 + 5449920, Spec review fb2d280): graphitron-mcp answers every tool from the fact store, and its reactor dependency set closes on the store’s schema. Thirteen tools, one resource and one prompt read four generator-side projections reached through the language server’s Workspace; ten tools, one resource and one prompt now read relations, and the module compiles against graphitron-model plus org.jooq:jooq and nothing else in the reactor. Four goal properties, each asserted in StoreClientBoundaryTest: one reactor compile edge, no reference to the language server in either tree, no read of the FieldClassification / TypeClassification / TypeBackingShape taxonomies, no read of the walk_ family, and no connection the module opens for itself (DevQueryExecutor excluded by name, its connections being to the consumer’s own database). Six artifacts left a published module’s compile and runtime classpath, graphitron-lsp and graphitron among them, and the reactor now builds graphitron-mcp before graphitron-lsp. The surface shrank before it migrated: edges was dropped rather than ported (its forward half is what schema answers, and carrying the costume for the reverse half meant porting five labels, a six-permit node model and a memoised map inversion), and services / conditions / records collapsed into one code tool with a kind selector, the three having shared an argument schema and a census and differed by a WHERE clause each. CatalogFacts deleted with its last reader, which the LSP fact-store item could not do from its own side. The ninety-odd exhaustive classification arms had no store-side counterpart because nothing consumed them as a union: seven relation reads replace them, the hard one being the backing class, whose closure is materialized at capture cadence after the specced per-request recursion measured 369 seconds and returned nothing on H2. Wire deltas are named rather than discovered, the breaking one being schema’s `kind vocabulary moving from permit names to store classifiers; four bindings go absent rather than have this module re-implement a model rule. Two substrate findings graduated to docs/architecture/explanation/fact-model.adoc (one projection per grain; read a window-function or recursive view once and pair on its key) rather than dying with the item file. Independent-session In Review → Done review: clean full reactor under mvn install -Plocal-db (14 modules, graphitron-mcp 145 tests, 0 failures), no code-string assertions on generated method bodies, user-facing-doc check clean over three docs/ files with the manual’s tool table build-enforced against the registered surface by ServerInstructionsTest, and the boundary guard verified non-vacuous by planting a generator reference and watching it fail. Two findings, neither held against the gate: the guard’s generator-package needle covers no.sikt.graphitron.rewrite. alone where the module publishes four more packages, filed as R716; and the declared retirement sweep had missed three live roadmap bodies, corrected in the approval commit, the consequential one being lsp-structural-consolidation.md’s slice 5, whose torn-read argument rested on `graphitron-mcp being a second concurrent Workspace reader.

  • R712 (84e19b4; Backlog → Spec 850c5bf, Spec revisions 76c8103 + e8793e1 + 5b609c8 + cc1fedd + e6b58c2, Spec → Ready c0abe6f, Ready → In Progress 3fd8048, In Review f108cd0): the fact store’s three strata are named on the page that already taught the discipline following from them. Capture transcribes facts from a corpus, derivation computes further facts from captured ones, queries read facts to serve a goal; a new fact-model.adoc section states that axis, the mechanical recompute test that decides membership (a row recomputable from captured facts alone is a derived fact and must not be captured), and the numbering rule that keeps "stratum one/two/three" this axis while an unnumbered "the X stratum" stays a DDL section or a within-family layer. The assignment covers all thirteen meta_family rows plus the prefix-less diagnostic placement exemption, deferring to the roster rather than restating it, and the verdict rule extending the test to conclusions (a transcribed verdict is stratum one exactly while the store does not hold the inputs it was computed from) lands the five verdict residents apart rather than together: graphql_syntax_error stratum one on an unparseable input, graphql_schema_error split along its own stage column (REGISTRY one, ASSEMBLY two, every assembly check being a predicate over captured rows), lint_ stratum two although lint_finding is a table today, and build_warning_ disclosed as unsettled at family grain because its arm carries no rule and shares a channel rather than an input set. Three enforcer lines rather than one, since the decode-does-not-displace-transcription claim is gated (FactSchemaGateTest.theDecodeDoesNotReplaceTheTranscription, with the in-place-rewrite gap stated) while the recompute test and the assignment are gated by nothing, which is why both named inversions sit green today; the page argues with its own "a rule without an enforcer is not on this page" preamble in the open rather than satisfying its letter. Retired: "authored versus effective" as names for the store’s contents, and the blanket "the authored picture is the anti-join" defence, which is false for the one provenance relation marking a row an expansion rewrote rather than added. The sweep took the pre-expansion-value sense and left the which-rows-an-expansion-contributed sense, across the DDL comments that render into the schema reference (graphitron_field_synthesis and its Macro synthesis provenance header, three intent_ view comments reading its text column), both explanation pages, MacroCapture, SdlFactCapture and the two capture tests; effectiveFieldTypeexpandedFieldType is the only identifier renamed, authored_type_sdl and the intent_authored_* views deliberately left because the stratum reading predicts they become unnecessary rather than merely misnamed. Corrected in the same pass because the DDL outranks the page: the retired keying rule’s twin in the Derived stratum: claims block comment, the Semantic stratum section header and the one table comment pointing at it by name (repointed at graphitron_undecoded_argument on the overflow property the two comments already share, not on a decode the graphql_ side does not perform), and the graphitron_ and intent_ roster charters. The Diagnostics stratum header is left whole under the numbering rule, the preferred of the two options offered, and the two pipeline-overview.adoc sites folding four diagnostics families into "the derived strata" (a section heading and the mermaid node above it) drop the stratum claim instead. Three sibling plans restating the frame in the retired word are converted. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db (14 modules, 5725 tests, 0 failures), no code-string assertions on generated method bodies (the delivery changes no assertion at all), user-facing-doc check clean over both .adoc files, and the retirement sweep re-run independently over the DDL, both pages and the four Java files, every surviving hit landing on a stated fence. Two implementer deviations both improve on the spec’s own text and were disclosed at the flip: the intent_ charter says the depth ordering’s upper two layers are the derivation ones where the spec’s Implementation section wrote "lower", matching the spec’s own argument section; and "the transcription strata above" became "transcription relations", a plural the numbering rule exists to stop. One finding fixed in the approval commit under the spec’s own "enumerate them from the file rather than from this list" instruction: the numbering paragraph claimed five unnumbered stratum uses on the page when the same commit had replaced one of them, leaving four, which is the unguarded census the section itself argues against.

  • R638 (f1801f0 + a28db9d + 14f9031 + 18d883b + 14c2ba7 + 274e2e9 the reachable slices, 5cdbaa6 + 758821d + 4085989 + 6b201a2 the substrate views, f6eba9b + 0d4e2d1 the doctrine repointing; the item ran from 2026-08-12 across many sessions and its earlier slices predate this clone’s history; 21e1555 restructured the body for the gate, 33fe9f0 + 17d376c recorded the runtime edge and the seam figures, In Review df5f307): the language server is a fact-store client. Every capability, completion, hover, definition, inlay hint, diagnostics and code actions, resolves a cursor with tree-sitter and answers from relations; there is no model between the dispatch switch and the store, no per-round projection for a handler to consult, and no state a handler shares with another beyond the store itself. The item’s own nominated structural test was graphitron-lsp’s pom, which named `graphitron and imported twenty-one generator types: it names graphitron-model now, the graphitron edge is test-scope with a comment saying what widening it would cost, and no main source imports a generator type or graphql-java by import or by fully-qualified name. Six of the last crossings were accounted for one at a time rather than moved wholesale: four were rules rather than models and moved down to graphitron-model (SourceUri, whose forward and reverse trips had been defined in two modules with nothing holding the halves together; ConstantReferenceGrammar; FieldSourceSigilGrammar; InferredDirectiveArgs), and two were the directive vocabulary’s and were replaced rather than moved. That replacement is the load-bearing change: the startup parse of graphitron’s bundled directives.graphqls into a graphql-java registry is gone, DirectiveSurface reads the same four relations an author’s own declarations land in, and the bundled-versus-user split becomes unrepresentable rather than fixed, so an author’s own directive gets the same nested descent @reference gets. The vocabulary became per-graph state reloaded when capture swaps, and the old constructor-time drift guard became DriftDetectionTest, which asserts the same invariant against a capture of the shipped file without taking an editor down over a graph nobody has captured yet. Diagnostics ride the capture cadence rather than the keystroke, which retires the tree-derived type index and the cross-file fan-out it aimed; a whole recalculation costs one statement per graph rather than one per value an author wrote, pinned by DiagnosticsStatementCountTest including that the count does not track the document’s size. In place of the declined shadow-parity gate, TriggerDispatchMatrixTest partitions 13 sealed Trigger leaves x 6 LspSurface arms into answered / declared-no-answer / unimplemented, drawing its universe from getPermittedSubclasses() so a new trigger fails the build until every surface says what it does with it, and the capability inventory’s empty arms become declared facts rather than silence. A deferred census ran at the gate and answered "nobody": CatalogBuilder.buildSnapshot ran on every generator pass and reached one production statement in the reactor, so the classification projection was a model with no consumer being rebuilt on every build, and it deleted here rather than in a follow-up, taking FieldClassification, TypeClassification, DirectiveShape, InputValueShape, LspSchemaSnapshot and the @ProjectionFor coverage apparatus with it. The line counts moved in both directions and the item said in advance that saying so was the point: graphitron-lsp main 9,119 → 14,203 with the facts package (3,231 lines of queries and their row types) being the seam itself, against rewrite/catalog 4,008 → 1,536 and its seam 3,232 → 997. Three named exemplars in the principle docs were repointed rather than deleted, fact-model.adoc’s one-slot provenance case to `graphitron_node and its freshness paragraph to why no consumer carries the axis at all, and both pages now name each consumer’s own coverage gate where they had named a projection seam that no longer exists. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db (14 modules, graphitron-lsp 606 tests, 0 failures, 3 skipped), no code-string assertions on generated method bodies anywhere in the delivered tests, user-facing-doc check clean (the item makes no user-manual change, and the two author-visible behaviours it does ship, the diagnostics cadence and a never-captured session completing nothing, contradict no page that exists; lsp-inlay-hints.adoc’s "immediately on next paint" was checked against the new collector and still holds, because site collection reads the buffer). Every deletion in the declared retirement list was verified absent and every declared survivor verified in its stated new home; the sweep found one surviving citation across all prose surfaces, a `graphql_type DDL comment naming CatalogBuilder.projectTypeDefinitionLocations in the present tense, fixed in the approval commit by dropping the dead exemplar and keeping the fact. Two bookkeeping items handled here rather than held: DriftDetectionTest’s "the full table lives in the spec" comment repointed at `CanonicalOverlay.overlay() before the spec file it named was deleted, and R680’s depends-on cleared, which the roadmap tool fails the build over once a shipped dependency’s file is gone. The three design calls the implementer left open were reviewed and all three stand as shipped. Left for their owners rather than acted on: three roadmap items whose premises this item’s deletions falsified (lsp-structural-consolidation.md being the strongest case for Discarded, plus mcp-snapshot-axis-key-naming.md and lsp-compile-diagnostics-publish.md), eleven more needing a line edited, and the 29-type test-scope edge that fact-store-test-harness-consolidation.md owns.

  • R711 (b503a79; Backlog → Spec and the spec review 84fe769, Spec → Ready 0d69e4d, Ready → In Progress 06b343b, In Review 650b771): nodehood derives from the captured facts of two corpora instead of being decided inside the SDL walk. Capture had exactly one place where the rows it wrote about one corpus depended on another’s contents: MacroCapture.expandFederationKeys asked NodeDeclaration.isNodeType before synthesizing a federation @key, and that predicate conjoins the SDL claim (@node, or @table plus implements Node) with __NODE_TYPE_ID / __NODE_KEY_COLUMNS read by reflection off the backing jOOQ class, so editing a generated class made an unchanged .graphqls file write different graphql_ and graphitron_ rows. The reading that fixed it is a misplaced stratum rather than an inverted polarity: the rule consumes captured facts and produces a fact computable from them, which is stratum two running inside stratum one and landing its output where nothing tells it apart from a transcription. The rule is now four views. intent_inferred_node_type is the inferred population, an @table binding through intent_bound_table at candidates = 1 plus an implements Node plus a sql_node_metadata row on the resolved table with no intent_node_metadata_defect row for it, carrying that table as witness columns so the identity question this item does not answer becomes a join rather than a re-derivation; it stands on the @table arm alone rather than on the intent_resolved_type_binding reduction because the live predicate reads the directive’s presence before it probes anything, so a routine-return binding is not nodehood however well-formed that table’s metadata is. intent_node_type unions that with graphitron_node, the UNION dedupe dissolving the predicate’s declared-wins short-circuit along with the provenance column that would have asked for it. intent_synthesized_federation_key is federation’s node-entity rule, a graphitron_link.url prefix match, membership, and no authored key whose decode is exactly the single path id, which transcribes the live rule’s deliberate asymmetry where a malformed fields: argument decodes to no field rows and therefore reaches its detection instead of suppressing synthesis on a parse failure. intent_federation_key composes authored with synthesized at the authored grain, UNION ALL with a NULL ordinal on the derived arm so two authored @key(fields: "id") applications at distinct ordinals stay two rows. The second spelling of the well-formed-metadata conjunction beside intent_resolved_node_key_column’s `JOOQ_METADATA tier is accepted rather than extracted, each view comment naming the other so the duplication is stated rather than latent. Capture surgery: MacroCapture handles @asConnection alone and expand() takes no arguments, SdlFactCapture.baseSites dies with the ordinal-taking captureTypeDirective overload while ordinalsByType keeps its own unrelated reason, and NodeDeclaration leaves the capture API outright, off FactCapture.run, runWithDetections and every capture overload, leaving jooq as the only catalog-shaped input with a javadoc saying nothing may add a second. graphitron_type_directive_synthesis retires, its CHECK having closed its vocabulary to the one macro that moved, and graphitron_type_declaration_synthesis’s `CHECK drops FEDERATION in the same sweep. ReachabilityRows.seed replaces its graphitron_node arm and its over-approximating @table-plus-implements arm with one intent_node_type arm, retiring the stopgap its javadoc named and narrowing intent_type_domain by exactly the types whose binding is ambiguous or whose table publishes no or malformed metadata. The gate is the deliverable rather than a nice-to-have, because no foreign key could have caught the violation: a key constrains references and the schema already refuses to model SDL-to-jOOQ resolution as one, while a cross-corpus read adds no reference and instead changes which rows exist. CaptureCorpusIsolationTest captures one registry twice, once with the catalog and once without, and requires every graphql_ and graphitron_ relation identical, the relation set enumerated off the generated model by family prefix so the next capture-time cross-corpus read fails it without being named in it; it carries a non-vacuity control asserting the catalog arm really captured one. Beside it, NodeTypeTest (13 cases) and FederationKeyTest (16) pin both views row-in-verdict-out in the module whose DDL declares them, NodeTypeShadowTest binds intent_node_type to NodeDeclaration.isNodeType at the membership grain because the intent_type_domain shadow cannot (a closure is not injective on its seeds), FederationLinkPrefixPinTest holds the view’s SQL literal to FederationSpec.SPEC_PREFIX since a view cannot bind a query parameter, and FederationKeyDerivationTest takes the six re-aimed capture cases out of MacroCaptureTest, whose javadoc the same commit rewrote to say the class handles @asConnection alone. CapturedStore.ofPipeline now passes the catalog through to capture rather than handing the walk a catalog-bearing predicate beside a catalog-free capture, which was the shape that let a fixture disagree with production about nodehood unnoticed. fact-model.adoc gains the cross-corpus rule with this gate as its named enforcer, amends its disclosed-gap paragraph to name the covered subclass rather than to shorten its list, and gives "moving the rows is a schema change" its first instance. Nothing reaches generated code, the emitted schema’s synthesized @key having always come from KeyNodeSynthesiser’s registry rewrite. Retired: `expandFederationKeys, MACRO_FEDERATION_KEY, graphitron_type_directive_synthesis. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db (14 modules, BUILD SUCCESS, every one of the eight named anchors run and passing), no code-string assertions on generated method bodies anywhere in the delivered tests, user-facing-doc check clean over both .adoc files (contributor-facing architecture pages, no roadmap-internal markers), and the retirement sweep found all three declared terms absent from every prose surface outside the item file itself. One finding, filed as R730 rather than held against the gate because it breaks no contract and costs eight one-line edits: the narrowing left seven dead NodeDeclaration imports and, more consequentially, a {@link} in WarmStartRefreshTest naming a capture overload whose parameter list still ends NodeDeclaration), a dangling reference that the reactor’s javadoc reference gate is built to reject but structurally cannot see, the javadoc goal reading main sources only. Two improvements noted for whoever wants them, neither a gap the contract left: the isolation gate’s non-vacuity control asserts sql_node_metadata is non-empty overall rather than for film_actor specifically, which is the table the differential actually turns on, and the prefix pin asserts containment, so a SPEC_PREFIX shortened to a substring of the view’s literal would still pass.

  • R680 (4d64a26 graphitron-model gains test sources and the M0/M1 harness, 42b1477 three tests go home and the two seeding prototypes reconcile, f1f2238 the capture handle becomes graphitron’s shared harness, 6ab3c4a the build level, 22e680c the guard, b481d60 + eae6407 + a423457 + f6e9c34 four seeded movers, 6b18977 + 68b4255 + c2e84d5 + abdf559 + 274e2e9 + 2120b2e + c8e9dc9 seven anchors split along the subject, 05de542 the facts writers get a level, 8a8ec35 graphitron-mcp’s two fixtures become layers, 061dd07 graphitron-lsp’s fixture becomes a layer and the guard’s pending list empties; to Spec a6ec9a9, twenty-two spec revisions and two Spec → Ready sign-offs 513b3e3 + 8045e08, Ready → In Progress 54ceab7, In Review 46bf7c2, rewritten as a delivery record cb54c55): four modules had each arrived at their own way of standing a fact store up in tests, no two sharing a line, and most of those harnesses were not testing the module they lived in. The sort is on the subject rather than on the relation kind, which disagree in three places: what a relation returns given rows is graphitron-model’s question and gets seeded, what a `graphitron writer or crawler puts in the store is graphitron’s and gets a real capture, and agreement between a store-native relation and the transitional classification walk stays beside the walk so it retires with it. Five levels result, two homes and a local layer per consumer: `FactStores (the store’s lifetime, inMemory() and fileBacked(home) named rather than flagged) and SeededStore (named row-inserting helpers over the generated model tables) in a new graphitron-model test-jar, FactWriters (the four shipped facts writers over a store somebody else opened), CapturedStore (a real FactCapture run, layered closure form over handle over primitives, named arms rather than flags) and BuiltStore (a real buildOutput() into a store on disk) in graphitron’s, with `graphitron-lsp’s and `graphitron-mcp’s `StoreFixture and StoreBackedBuild surviving under their own names as thin local layers. Every level is usable without the one above it, and graphitron’s two stand on the model’s; a single reactor-wide fixtures module cannot exist at all, since it would have to depend on `graphitron while graphitron’s own tests depend on it. `graphitron-model went from no test sources to 26 test classes and a published test-jar, all structurally free of graphitron, and joined GuardScope.IN_SCOPE_MODULES so its sources meet the citation and vocabulary rules from their first commit. StoreFixtureGuardTest is the one new test: one recogniser (a test-source reference to GraphitronModelStore outside a harness), a failure message that routes by subject rather than naming a home, three declared harnesses, five exemptions on two permanent reasons, and a second case that fails on an entry whose file is gone or has adopted a harness, so a spent permission cannot linger. Cases that did not carry their weight were deleted rather than carried, under a mutation bar recorded in the commit messages: StoreReaderTest moved as three cases of five, one dominated and one a baseline restated. Retired: no.sikt.graphitron.rewrite.capture.CapturedStore, PENDING_MODULE_FLOOR, PENDING_SEEDING. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db (14 modules, BUILD SUCCESS, 329 tests in the new graphitron-model suite and every harness and guard test run and passing), no code-string assertions on generated method bodies anywhere in the delivered tests, no main source touched across all nineteen commits, no seeded half naming a graphitron type, the landing table verified class by class against the tree, the negative direction of the guard proved against synthetic sources rather than only ever observed passing, and exactly one consumer of a downstream fixture edited (GraphitronMcpServerTest’s URI-tail assertion, one of the two knowingly-moved assertions the item declared) with the `graphitron-lsp migration editing none. User-facing-doc check clean over both .adoc files (contributor-facing architecture pages, AsciiDoc table syntax, no roadmap-internal markers), and the retirement sweep found all three declared terms absent from every prose surface outside the item file. One finding, filed as R737 rather than held against the gate: roadmap-tool’s `SchemaReferencePagesTest still opens a store directly, in the one module GuardScope deliberately does not walk, so the guard’s promise is a module short of the tree and the spec’s claim that roadmap-tool "benefits by construction" names a reachability the adoption did not follow; `GuardScope’s javadoc still calls itself the prose guards' scope. Also noted, neither a contract gap: `StoreClientBoundaryTest’s reactor-dependency allow-list is a third changed assertion, though it is an allow-list built to be argued with and the change strengthened it by keying test-jars separately from jars, and the spec carries no per-slice landing SHAs, which this entry supplies instead.

  • R732 (6870aab the column-match sweep reads its claim view once, b3e2f7f the javadoc gate stops forking, 9ea52f8 class-level test parallelism in graphitron, 3f85cff the H2 materialized-view ruling gets a permanent home; filed 86eed92, Backlog → Spec 641f2d4, spec review 1e612db + 000da7e + a6d95a1 + 53a57b3 + 476c77c, Spec → Ready dafca84, Ready → In Progress 90db5b6, In Review 5e9ec17): the build’s wall clock, recovered once from three measured slices. Trunk CI had gone from a 5 minute median to a 15 minute median in seven weeks while the suite grew 21 percent, so the shape was wrong rather than the volume. Two thirds of a build was the test phase and the largest single share of that was H2 re-evaluating the fact store’s own derived relations: intent_column_match_claim collapses its matches with a window over intent_field_column_scope, and a window sees its whole partition whatever predicate the reader applies outside it, so ColumnMatchShadowTest reading it once per graph over a 55-graph store paid all 55 graphs' rows 55 times, quadratic in corpus size. The query now projects graph_name, runs once and pairs per example through fetchGroups; the rows are identical because both anti-joins already correlated on graph_name themselves, so the outer predicate only ever chose which of those rows a caller saw, and the sweep’s assertions are unchanged, which is what proves the pairing. Separately the check-link-references gate bound the javadoc goal, whose executePhase=generate-sources forked a lifecycle that had already run at verify; Maven runs that fork before the mojo body evaluates its skip parameter, so graphitron-sakila-example paid for five duplicated graphitron:generate executions and then skipped the goal it forked for. javadoc-no-fork is the same mojo without the fork. And graphitron became the reactor’s first module to run test classes concurrently (methods stay same_thread, since a class owns its @TempDir and H2 store while two methods of one class share theirs), with @Isolated on the two classes that rebind ClassificationTrace’s process-global writer. On one 4 vCPU sandbox measured at both ends, `mvn install -Plocal-db went 9m06s to 6m44s and -T 1C 8m26s to 5m58s, with the graphitron module 181.2s to 70.6s. The fourth deliverable exists because the item file does not survive Done: the ruling that CREATE MATERIALIZED VIEW is unavailable on H2 rather than merely unattractive, and the four defects behind it, moved into `fact-model.adoc’s derived-reads section with a pointer from `graphitron-model.sql’s header. What this bought back once it does not hold; the guardrail, the unmeasured slices and the derived-read storage policy are R733’s, and the leaf-coverage trace’s schedule-dependent composition is R736’s.

  • R672 (79c81bb the implementation, c822de0 the README regeneration; filed 304d00a, Backlog → Spec 0fac740, Spec → Ready 7073bd6, Ready → In Progress ca4384d, In Review db9574c): every built-in scalar the emitted schema references is registered on the builder, independent of what the author wrote. A schema declaring applikasjoner: [Applikasjon] @asConnection and naming Int nowhere generated without complaint and then failed at consumer startup with type Int not found in schema (issue #527, against 10.0.0-RC30), because built-in scalars are not auto-registered on a programmatic schema and the generator sourced its additionalType set from the classification walk, making it "scalars an authored coordinate reached" rather than "scalars the emitted schema references". Connection synthesis runs after that walk, so the pagination surface’s scalars were never candidates. The minter now demands and the classifier produces: every promotion that registers a schema form sweeps that form’s field and argument types for named references and demands a row for each through one new TypeBuilder.ensureScalarRegistered, which no-ops both on a name already registered and on a name that is not a scalar, so call order is irrelevant and the sweep carries no type-axis knowledge that could disagree with the classifier’s. Row construction stays in classifyScalarType, the single producer the spec’s provenance finding asked for, over the assembled schema’s instance where there is one and over the graphql.Scalars constant for a spec built-in the assembled schema omits. What makes the sweep total is a type-system constraint rather than a comment: registerSynthesised takes <T extends GraphitronType & CarriesObjectForm>, and it is the promoter’s only typeRegistry.register call, so a form cannot be registered there without being swept, and a future synthesised surface referencing a new scalar demands it by construction. The declared-PageInfo corner falls out of the same routing, both arms of registerPageInfo now going through it. Backing it, a reference-closure guard over the emitted population: every scalar a schema-shape form references must carry a registration row or the build fails naming the coordinate and saying plainly that this is a generator defect, with registered-but-unreferenced still legal (the one-directional contract is untouched) and a name already carrying an UnclassifiedType demotion suppressed so the author-caused case keeps its richer report. Because the guard sweeps the rebuilt assembled schema, it also covers the rewrite-time first: Int / after: String arguments that no registered form carries, which is the overlap the promoter’s own javadoc declines to rest on. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db (BUILD SUCCESS, 611 test classes, 0 failures, 0 errors), the ratchet pin and the reported-failure execution test among them; no code-string assertions on generated method bodies anywhere in the delivered tests; user-facing-doc check skipped, the item touching no docs/ file; retirement sweep skipped by rule, the item declaring no Retired vocabulary section. Two deliberate departures from the spec, both verified and both standing. Slice 2’s population sweeps only the schema-shape forms where the spec also named survivor directive definitions and schema-level applied directives: directive type slots go through AppliedDirectiveEmitter.emitInputType, which embeds the graphql.Scalars constant inline for a spec built-in, a typeRef for a federation-namespace name, and a GraphQLString placeholder for anything else, so none of them demand an additionalType and sweeping them could only produce false positives; _FieldSet is sidestepped symmetrically, the guard and the generator’s registration filter both skipping _-prefixed names. And the spec’s finding 3 was wrong that String and Boolean sat behind the same hole as Int: graphql-java always adds @deprecated(reason: String) and @skip(if: Boolean!), SchemaReachability.seeds has seeded survivor directive argument types since 2026-07-24, and no always-added built-in directive carries an Int argument, so Int was the only scalar ever unregistered. The corrected inline comment on scalarRegistration_unreferencedSpecBuiltInIsNotEmitted encodes that, and the pipeline pin says in its own comment that its String / Boolean assertions state the property rather than a behaviour change, which is the honest reading. The unspecced CarriesObjectForm capability marker is judged earned rather than scope creep: it is the mechanism the intersection bound needs, it follows the EmitsPerTypeFile precedent of an orthogonal marker outside the sealed hierarchy, and collapsing the object generator’s six-leaf instanceof chain and the builder’s five-arm form switch into one read each is a consequence of single-sourcing rather than the motive (GENERATOR_LEAF_INSTANCEOF_SITES 72 → 69). Its widening to include NestingType is behaviour-preserving in resolveSynthesisedConnectionTypes, whose input is absentMinted(), and a NestingType is classified from an assembled-schema object and so can never be absent from it. Two findings, neither held against the gate. The spec body was never updated to reflect what shipped, against workflow.adoc’s "implementer writes code, updates the plan" step: the In Review flip changed `status: and last-updated: alone, so the body still read as a forward-looking plan and recorded neither departure, which is why this entry carries them; rework was declined as ceremony over prose the approval deletes. And the delivery retired ObjectTypeGenerator.graphqlTypeFor without declaring it, leaving relevance-ranked-search.md instructing its future implementer twice to add an arm to a deleted private method, repointed at CarriesObjectForm in the approval commit.

  • R706 (77c06c3 the delivery, which also flipped In Review; spec landed inside 9d9dc46, Spec → Ready bd73a70, Ready → In Progress b1b40e0): a build that starts while mvn graphitron:dev holds the workspace’s fact store used to stall for about two minutes in silence and then continue as if nothing happened, and one of the two stalls under it was genuinely unbounded. AUTO_SERVER=TRUE is gone from the store’s URL: H2’s mixed-mode liveness probe read a handshake off a socket with no read timeout, so a suspended holder or a stale lock file whose ephemeral port something else took over blocked the opener forever, under a JVM-wide monitor, with no property reaching it; without the flag H2 writes no lock file, takes the OS file lock, and refuses a held file in well under a second, straight into the in-memory fallback that was already written. Nothing in-process changes, which is where the sharing that matters lives (a reactor build’s modules share the Maven JVM, and the LSP and MCP read through StoreReaders minted by the holding process). The capture no longer waits where waiting buys nothing: two SET LOCK_TIMEOUT statements bracket the store_graph anchor upsert, narrowing to a two-second budget (FactCapture.ANCHOR_LOCK_MILLIS) and restoring GraphitronModelStore.FILE_LOCK_MILLIS, now a named public constant so the two cannot drift; the store-global rows after the anchor keep the generous budget the tests prove they keep. A lock timeout is not retried (the cause-chain split in timedOutOnALock keeps the deadlock retry), and the demotion reaches the console once, at warn, from both layers, saying what held the store, what the run did instead, and that the generated output is identical. docs/manual/how-to/dev-loop.adoc carries the symptom as the user meets it; R757 files the hard-killed holder’s other half, the stampless file that never warms again. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db (BUILD SUCCESS, PersistentStoreTest 12/12 including the forked-holder open bound); no code-string assertions on generated method bodies anywhere in the delivered tests; user-facing-doc check clean over both touched .adoc files; retirement sweep clean, the one surviving AUTO_SERVER mention being fileUrl’s deliberate refusal rationale; the spec’s load-bearing claims re-verified against the tree (the store is the reactor’s only H2 URL builder, writeGraph touches only the anchor row, and no module opens the file cross-process).

  • R743 (227a199 the gate deletion, f8b7dab assembly as the gatherer’s own stage and the rooted traversal, bfa41ab the anchor tables, 7c6d938 the rework pass; In Review 8403b04, body reconciled b892ae1, first Done gate → Ready d4eb242, In Review again a23af39): the SDL fact gatherer is a five-stage pipeline and the walk_ membership gate is gone. intent_authored_claim_conflict is total over the authored claims with each consumer applying its own population join (the build-error surface joins intent_type_domain, the editor reads the view ungated), so walk_claim_domain_type, walk_claim_domain_field and ClaimDomainRows are deleted and WalkReach dissolves into the typed ClassifiedRun discriminator. Assembly is the gatherer’s own stage and its ASSEMBLY verdict judges the pre-synthesis registry, so an author is never blamed for a declaration graphitron’s rewrite injected; where the pre-synthesis registry assembles and the post-synthesis one does not, the author’s facts are captured before the build fails on graphitron’s own defect. Stage 5 is a rooted traversal (ClassificationDomainCapture) writing intent_type_domain from SDL-only seeds scanned over every implementing type, replacing ReachabilityRows’ SQL closure; the node seed is `implements Node alone, so a declared node no field reaches gains diagnostics instead of vanishing, and the domain is empty on a refused assembly, read together with the verdict. Stage 4 landed as the graphql_*_coordinate anchor family rather than the filed composed-census transcription: existence and attributes split per relation, the whole FK web re-pointed onto the anchors, SdlCoordinates owning every coordinate’s first-wins claim, and capture’s merge pinned against graphql-java’s composition by SdlCoordinateCensusTest at all four grains with every merge-ordered ordinal family pinned by value on out-of-order fixtures. Independent-session In Review → Done review (second pass, the first having held the retirement sweep and the census pin): full reactor green under mvn install -Plocal-db; the sweep clean, the two surviving walk_claim_domain mentions being deliberate deleted-past prose; the census pin verified equality in both directions at all four grains and all five ordinal families; no code-string assertions on generated method bodies in the delivered tests; user-facing-doc check clean over the three touched architecture docs.

  • R746 (dbc619a the delivery; filed and Backlog → Spec c02d901, spec revisions a41084a + 9dae6ee + 95ae64e, Spec → Ready 7f2ff35, Ready → In Progress 31a1844, In Review a76d0e2): a registered materialization target may now be derived from another registered target, because the refresh order is derived from the store’s own catalog instead of being absent. R742’s registry recorded no ordering, which was correct for the two rows it landed (neither view reaching the other’s target, both closures base tables only) and not correct in general: the moment a registered view reads a registered target, an unordered materializer fills one target from the other’s stale or empty rows, and R733’s third measurement pass met that case on the first attempt at a third registration. What it reached the schema author as was not a wrong number but a confident wrong instruction to change their own SDL, which is the strongest argument the item had. Three shapes were on the table at the Spec pass and the settled one is the middle: the edges are derived rather than authored, because a gate comparing an authored copy to a parse can only stay green by having the authored copy miss the same edge the parse misses, so the copy buys no protection and adds the drift shape the fact model refuses; and the derivation gets a relational home rather than staying a Java computation, because three readers ask it (the materializer orders by it, the gate asserts over it, a person debugging a refresh can SELECT it) and because it is a function of the DDL alone, so it is parsed once per created store rather than per refresh. meta_materialize_dependency (source_view_name, depends_on) is the relation, both columns referencing the registry’s key, the pair its primary key, a CHECK refusing the length-one cycle declaratively, and the family’s first machine-written resident, its charter sentence saying so. MaterializeDependencies.populate is its one writer: for each registration it takes the source view’s stored INFORMATION_SCHEMA.VIEWS definition, parses it with jOOQ’s parser, collects the relations read off the rendered query object model rather than off text, recurses through a read that is an unregistered view, emits a row for a read that is a registered target, and stops at base tables, with a definition the parser refuses failing boot loudly and a view reading its own target refused by name. Collection leans on H2 normalizing every real relation reference to "PUBLIC"."NAME" while aliases and CTE names stay unqualified, which is what keeps an alias sharing a relation’s name from minting an edge; the routine runs where the schema is created, a warm store’s rows having been written by a boot off the same stamped DDL. Materializations.refreshOrder returns its own RefreshOrder type rather than a second List, Kahn over the rows with an alphabetical tie-break so a row-free relation yields exactly the census order byte for byte, consumed by both refresh and refreshAll, refusing a cycle defensively with the cycle named. The build-time acyclicity gate in MaterializeRegistryGateTest is the invariant’s home and replaces theRegistryNeedsNoOrderingYet along with the word-boundary regex closureOf it stood on; beside it, the one cross-boundary direction a catalog parse can see is gated (no hand-written derivation is a registered target) and the direction it cannot, a hand-written jOOQ derivation reading a registered target, is disclosed on FactCapture’s stratum comment rather than enumerated by hand. DDL also gains `UNIQUE on meta_materialize.target_table_name on its own merits, and fact-model.adoc’s materialized-view ruling gains the sentence reconciling its objection to hand-maintained refresh ordering with an ordering single-sourced from the booted store’s own definitions. Retired: `theRegistryNeedsNoOrderingYet, MaterializeRegistryGateTest.closureOf. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db (BUILD SUCCESS, zero failures, MaterializationOrderTest 8/8 and MaterializeRegistryGateTest 6/6), no code-string assertions on generated method bodies anywhere in the delivered tests, user-facing-doc check clean over the one touched .adoc (contributor-facing, no roadmap-internal markers), and the retirement sweep found both declared terms absent from every prose surface outside the item file and the docs render tree. Completeness was verified by discrimination rather than by the green build: with the topological sort bypassed in a scratch patch, the three behavioural cases fail (aDependentRefreshesAfterItsPrerequisite, its refreshAll sibling, and the named-cycle case) and so does the pipeline gate’s new fixture registration on real store machinery, failing exactly as intent_fixture_binding unpopulated. The spec’s "changes no timing whatsoever" claim survives the added boot-time parse: GeneratorDeterminismTest measured 16.19s against R742’s pinned 16.24s, the production walk parsing two view definitions with no recursion. Two non-blocking notes, filed as R761 rather than held against the gate: deriveDependencies runs after stamp in openAt, so a process dying between them leaves a store a later boot accepts as warm with an empty dependency relation, silently degrading to alphabetical order, which deriving before stamping would close; and theRefreshOrderRespectsEveryDependencyRow is vacuous on the shipped DDL, zero rows meaning it passes with the sort bypassed, so the pipeline fixture rather than that gate is the production-machinery evidence.

  • R760 (608f047 the delivery, 83d45b1 In Review; filed inside abaa666, Backlog → Spec 1e26a84, Spec → Ready cce3426): intent_class_assignable is deleted rather than repaired. The all-pairs transitive closure over jvm_class_supertype had no production reader, did not return at all on a store holding one class name under two classpath entries, and took seventeen seconds on a census that holds no duplicate name at all, so what the view offered its first reader was a trap rather than an answer. The measurements are what settled the verdict against all three repairs: the rewrite drafted at filing is not semantics-preserving (re-attaching source_name by class name folds one entry’s hierarchy into another’s, invisible on any census whose duplicate entries hold the same classfile), the corrected form that anchors attribution at the first hop is exact and runs over two minutes, and materializing moves the seventeen seconds onto every build’s capture cadence for a relation nobody reads. The cost is the path guard enumerating simple paths at census scale, not the duplicates; the non-termination is the duplicates, and it reproduces at forty stated rows, so the fixture scale ClassAssignableTest was accused of was never the gap. Four in-SQL comments are amended so no surviving prose reads as though a closure were live (intent_name_matched_key_pair re-citing intent_class_member_slot, intent_type_backing_class stating its own no-route-column rule, intent_delivery_container speaking of a closure rather than the closure, and jvm_class_supertype recording the census’s ordinary posture plus the two constraints for whatever reinstates one: recurse over the pairs the rows denote, seed from the names a consumer asks about). intent_authored_field_claim’s `lookup_bearing recursion carried the same defect under the same UNION ALL and is deduplicated in the same commit, recursing over an input_object_field_edge CTE, consumption being through EXISTS so no answer changes. fact-model.adoc’s recursive-view paragraph keeps its general rule and replaces a worked example that had the guard’s cost backwards with the three measured rules. Retired: `intent_class_assignable, INTENT_CLASS_ASSIGNABLE, and the whole of the view’s comment. Independent-session In Review → Done review: full reactor green under mvnd install -Plocal-db (BUILD SUCCESS, all four roadmap-tool gates clean, check-schema-identifiers resolving 17 pages against 214 relations); no code-string assertions on generated method bodies in the delivered tests; user-facing-doc check clean over the one touched .adoc; retirement sweep clean, every retired phrase and both spellings of the relation name surviving nowhere outside item bodies, and the surviving indefinite "an assignability closure" comments in ClasspathScanner, ClasspathScannerTest and CompletionData reading as the posture jvm_class_supertype’s amended comment now states rather than as a live relation. Completeness verified by discrimination rather than by the green build: the deletion could not ship half-done because `FactCaptureAgreementTest.everyRelationIsRegistered gates both directions, and the added aDeepInputChainWhoseLevelsRepeatTheirFieldTypeStillAnswers was re-measured with the dedupe reverted in a scratch patch, giving TimeoutException at 30 s where the deduplicated form answers inside a class that runs 18 cases in 5.5 s, the other seventeen unaffected. One non-blocking note: the third in-SQL amendment (intent_delivery_container) is scope the spec body did not enumerate, disclosed in the delivery’s commit message and serving the item’s own stated goal, so it is recorded here rather than held against the gate.

  • R759 (452c497 the delivery, 821890c In Review with the re-measurement; filed and Backlog → Spec 969803b, spec review 2d16bf5, Spec → Ready d972d9e, Ready → In Progress a1b17e9; Done-gate review d865b68 requesting the retirement sweep’s residue, cleared and approved in this commit): no stored column spells a file as a URI, so no store boot compiles Java. The wire spelling left storage. rejection_validation_error.file, lint_finding.file, build_warning_no_rule.file and javac_diagnostic.file now hold the path their writer read, the diagnostic view derives file and directory from paths in all seven arms, and the CREATE ALIAS carrying inline Java source that H2 compiled with javac on every schema execution is deleted, which makes ModelCodegenDriver’s "the schema declares no routines" comment true. The two boundaries whose protocol names a document by URI convert at their own edge instead: `DiagnosticFacts decodes a document URI to a stored path once and both replay entry points go through that decode, LintFixes compares the source name it already holds, and MCP’s DiagnosticFacets.Spelling gains an outbound render beside normalise so the group key, the files list and each entry’s location.uri publish a URI over a stored path. SourceUri is the one home for both directions, gaining the null guard its javadoc promised and ofDirectory, which renders a directory as the URI of a file inside it with that segment removed: Path.toUri asks the filesystem about a trailing slash, so converting a directory path would put that directory’s existence on the wire. ValidationReport.sourceUris retires with addCanonical and canonicalUri, its javadoc’s LSP short-circuit having predated store-based diagnostics. Nothing moved on any wire, which is what the five LSP published-URI assertions holding their expected values demonstrates. Tests: the alias-versus-Java parity case is replaced by DiagnosticFactsTest.noFileColumnSpellsAUri, a property that can fail, asserting no value in the four file columns and none the view projects onto file or directory begins with file:, over three fixtures covering all seven arms; DiagnosticsAggregateTest’s spelling pin becomes the `normalise(render(stored)) round trip plus a case pinning the published directory as the published file truncated, the one thing convert-then-strip and strip-then-convert disagree about; SourceUriTest pins both directions, the unparseable-path fallback and the trailing-slash trap. Measured on a 4 vCPU sandbox, statement by statement against a private in-memory H2: the alias was 28.8ms of a 159.5ms boot, twelve times the next most expensive statement, and it never amortised, each store paying its own compilation. That is 18.6ms and 11.7% off every store boot, which every consumer pays once per graphitron:generate, once per language-server session and once per MCP server start, and 387s to 349s end to end. Build green on all 14 modules, 6059 tests.

  • R633 (210bcb9 the delivery; filed inside 63db590, Backlog → Spec df65e10): two argument values escape the closed-vocabulary bet the diagnostics aggregate rests on, and now neither silently answers a different question. orderBy read as stringArg(...).map("key"::equals).orElse(false), so every value that was not exactly key meant count with nothing in the response saying the argument was ignored; a nested Ordering (count, key) now resolves through an of in Dimension.of’s shape, refusing an unknown value with both named, read off the argument map rather than through `McpWire.stringArg so a blank or non-string value is a refusal too, and the aggregate’s input schema declares the enum from Ordering.wireNames() so the pair is discoverable without a failed call. And DiagnosticsTool’s `severity sugar lowercased its argument where the shared where path did not, so severity: "ERROR" filtered and where: {severity: "ERROR"} matched nothing; a Spelling enum is now a fourth Dimension constructor argument on the four columns holding a case-folded store taxonomy (severity and source lower-case, kind and attemptKind upper-case with hyphens folded to underscores), applied in coerce, the one boundary both diagnostics tools share, so the sugar dropped its own toLowerCase and agrees with where by construction. matchesStored is untouched, which keeps an aggregate group key out of the normalisation path and the drill-down exact. The kind fold closes a second hole: an entry renders the stored INVALID_SCHEMA as rejectionKind: "invalid-schema", the spelling an agent has in hand to paste back, which matched nothing as a filter. The governing rule is the module refuses values from vocabularies it owns and normalises spelling for vocabularies the store owns, and the apparent tension between refusing "COUNT" and accepting "ERROR" resolves on the input schema: orderBy declares an enum to be no more lenient than, severity deliberately declares none, the store owning which severities exist. Five pins in DiagnosticsAggregateTest, two of them discriminating rather than regression-only. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db (BUILD SUCCESS, 14 modules, 6059 tests); no code-string assertions on generated method bodies in the delivered tests; user-facing-doc check and retirement sweep both skip, the item touching no .adoc and retiring no vocabulary. The design was read as a proposal rather than as approved design, the item having gone Backlog to In Review in one session at the user’s direction so the Spec sign-off was not independent, and it survives: the diagnostic view confirms every declared spelling (severity the 'error'/'warning' literals with compile rows projected through CASE WHEN j.kind = 'ERROR', source the 'schema'/'compile' pair, kind enum names plus two literals, attemptKind Rejection.AttemptKind.name()), and the four dimensions left AS_STORED that could plausibly have been folded (variant, stubKey, lintRule, attempt) are exactly those whose published spelling already equals the stored one, so the split is principled rather than the four columns someone happened to notice. Two non-blocking notes: DiagnosticsTool.entry still renders rejectionKind kebab-case by hand while Spelling.UPPER_SNAKE.render is the identity, so the module publishes two spellings of one taxonomy value and only the inbound fold hides it (a coherence wrinkle R759’s later render generalisation created, not this item); and the severity sugar still reads through McpWire.stringArg, so severity: 5 or severity: "" drops the filter and answers over every severity, the same silent-different-question shape this item closed for orderBy, left standing because the summary line at least stops claiming the filter. Both are Backlog material, neither is scope the spec named.

  • R771 (d27f54b the delivery, e233b1b In Review; filed 49c2253, Backlog → Spec 4699fdc, Spec → Ready 99962e4): grain and driving relation are one decision, and the doctrine has a durable home before a skill curates it. fact-model.adoc’s "One base, many views" gains a paragraph beside the one-projection-per-grain thesis stating the authoring sequence, name what one row of the answer means, drive from the relation owning that key, attach the rest by declared keys, and nest a child grain as a correlated `MULTISET on its own key or pair it as a second statement, with SchemaQueries named as the exemplar, the reverse order named as the smell, and the cost half handed back to the page’s own derived-reads rule rather than restated. .claude/skills/nested-jooq/SKILL.md is the procedure over it: six steps from naming the grain to closing with the pin, per-surface guidance splitting hand-written store reads (imitate SchemaQueries, verify over SeededStore) from @condition fixtures (the alias-bearing table.field(...) handle and the N x M contract, both cited to the manual where the reader plays the consumer, the latter with its gap stated as developer discipline rather than a build gate) from the emitter surface, which inverts: a command already carries its driving relation, so a renderer choosing a grain is an incomplete command rather than a query-authoring question. Four smells carry a live exemplar each. The vocabulary ruling holds throughout, drive from and the driving relation for the FROM-clause origin and never anchor, which the store’s DDL and LaunchSource.AnchorTable already spend on a join target. No gate over the skill document: the exposure is named instead, and the citation policy (doctrine pages and class names, never file:line, no restated measurement) is the mitigation. Independent-session In Review → Done review: full reactor green under mvnd install -Plocal-db (BUILD SUCCESS, 14 modules, check-schema-identifiers, check-adoc-tables, check-transient-citations, check-module-enumeration and check-adoc-xrefs all clean and the asciidoctor render passing); no Java changed, so the code-string-assertion rule is trivially clean; user-facing-doc check clean over the one touched .adoc, which is contributor-facing and carries no roadmap-internal marker; retirement sweep skipped, the item retiring nothing. Completeness was verified against the citation policy rather than against the build, since nothing scans the document: every one of the skill’s citations was resolved against the delivered tree, the four .adoc pages, the eight class names, ParentCorrelation.parentKeyColumns and LaunchSource.AnchorTable, SeededStore.derive, ReferencePathConditionFixtures.splitFilterParentIncluded and its concrete parameter types, intent_column_match_claim’s comment carrying the measurement the skill declines to copy, `FactSchemaGateTest.commentCoverageIsTotal asserting presence over relations and columns and nothing more as the skill states, and SchemaQueries’ own javadoc confirming the two-statements-paired-on-the-type’s-own-key shape the skill sends readers to imitate. The `description front-matter is byte-identical to the one the spec drafted, and the skill is live in a fresh session’s skill roster, which is the only evidence that the trigger text fires at all. Two non-blocking notes: the spec body shipped uncollapsed, with no "shipped at <sha>`" note on either deliverable and the In Review commit naming a pre-rebase `ca3d10c that resolves to nothing, so this entry is the only correct provenance record; and step 4 calls the harness table’s first row "this case" for all three store-read surfaces, which fits a graphitron-model derivation exactly and routes an MCP or LSP read through the table’s last row instead, a precision wrinkle in one clause rather than wrong advice, since both rows land on SeededStore.

  • R773 (d52d0b1f the mechanism and every consumer’s posture; Backlog → Spec 43e7477c, spec review findings 0e1cfe58, spec revision f0016510, Spec → Ready 12f8e450, Ready → In Progress f7986301): every fact-store read now runs under a ReadBudget its minter is forced by the compiler to state, and a statement that overruns one is aborted by the database rather than running forever. Before this no query the language server issued had any time bound at all, and StoreReader.read serializes on a single connection, so one pathological relation was head-of-line blocking every hover, completion and diagnostic behind a statement that might never return; both known cases were found by hand after somebody noticed the editor had gone quiet. graphitron-model gained ReadBudget (Bounded / Unbounded, rendering its own SET QUERY_TIMEOUT session command) and StoreAnswer (Answered / OutOfBudget, carrying the statement and the budget it overran), with the predicate keyed on H2’s vendor code 57014 rather than on SQLTimeoutException, because a lock timeout is the same type with the opposite remedy; GraphitronModelStore.reader() is replaced by reader(ReadBudget) with no defaulted overload, and FactCapture.timedOutOnALock gained the boundary that makes it wrong the moment a writer gets a budget. The LSP mints two readers per latency contract rather than one per consumer, the three StoreAccess doors partitioning the three read grains exactly as they already stood, so answering stopped delegating to answeringAll and a keystroke can no longer borrow the reader the drain owns. Each surface states its posture in an exhaustive switch; the two that are not merely absence are the diagnostics drain, which publishes nothing at all rather than an empty list that would clear the developer’s squiggles, and the directive vocabulary, which keeps its last good value rather than silencing every surface for every file until the next build. MCP fails the tool call instead, a turn-based caller reading an empty result as absence, with the semantic index the one site that keeps its live answer because that index is a pure function of a census the read did not finish reading. Nothing in the test tier asserts a duration: an overrun is provoked by swapping a relation for a view whose evaluation never terminates (RunawayRelation), and the door-routing case turns on H2’s own session setting rather than on a clock. Review verified both by mutation, re-collapsing the delegation and publishing an empty list, each of which fails its case. The two statement-count enforcers the plan named as a pre-existing gap are filed as R782.

  • R775 (d34fabf the delivery, 85b00ca In Review; filed inside R773’s spec review 0e1cfe5, Backlog → Spec db5c358, bounce-count correction 66197ce, precedent survey 1e98de1, spec review findings 44fbc24, author revision 3d3ed43, Spec → Ready a8de6d3): a Spec → Ready reviewer who withholds sign-off now lands findings rather than fixes. Before this the reviewer edited the plan body themselves, which put design prose into the spec that no session ever reviews as a draft: the next reviewer inherits it labelled settled, finds the defect in it, and the item bounces again. R769 is the worked failure, where 49% of the plan body under review was reviewer-written and two consecutive bounces landed on prose a reviewer had authored minutes earlier. Nine prose edits across three files, no code and no build gate. roadmap/workflow.adoc splits the single Spec --> Spec diagram edge in two, because review and revision have different actors and different guards; its reviewer-rule paragraph now says the rule rarely bites under the new flow, the author landing the revision and so being the last committer; a new Findings, not fixes. paragraph after "What each gate decides" carries the mechanism and the reason, that fresh context is the right asset for finding a problem and the wrong one for repairing it because the repairer holds the least context about the parts they are not repairing; a new Item-file-conventions bullet fixes placement (end of file, below every plan section), the one ### round per pass appended and never rewritten, and the response shape (a note directly beneath each finding, the finding’s own text untouched); and canonical-path step 2 splits into its clean and withheld branches with the preamble noting that a revision round is two commits rather than one. The srp skill’s Spec-stage Verdict bullet hands the reviewer a ## Reviewer findings section instead of an option to commit spec revisions themselves and carries the scoping test, that a stale count, a missing symbol, a broken link or a typo ride along in the findings commit while anything changing what the implementer will build is the author’s; its Implementation-stage rework bullet points at the same section for one convention rather than two; and "Template design intent" gains a paragraph exempting that bullet from the section’s own delete-instructions-first pressure, so a future editor does not optimise it away as ceremony. The roadmap skill’s transition table splits its Spec | Spec row to match the diagram. The reviewer-rule guard is untouched, Main.ALLOWED_TRANSITIONS storing targets as a Set and the skill table documenting it rather than feeding it, so both duplications are inert by construction. The convention was emergent rather than invented: five items in roadmap/ already carried a review section under three spellings, and this item picks the plurality and most recent one, deciding only the two things the practice did not agree on, placement and the shape of the author’s response. The measurement is R779, filed rather than folded in because the data does not exist until this item is Done and the file that would hold it is deleted at Done: the plan-body blame fraction across the next three bounced items against R769’s 49% baseline, with the falsifier restated there, that the change buys one extra session per revision round and pays for itself only if the round count falls. R779’s depends-on on this item’s slug was cleared at this gate, the front-matter validator refusing a dependency on a shipped item and its body carrying the ordering in prose anyway. Independent-session In Review → Done review: full reactor green under mvnd install -Plocal-db (BUILD SUCCESS, 14 modules, 6143 tests, 0 failures, with verify-roadmap-readme, check-adoc-tables, check-transient-citations, check-schema-identifiers, check-module-enumeration and check-adoc-xrefs all clean); no Java changed, so the code-string-assertion rule is trivially clean; user-facing-doc check and retirement sweep both skip, the item touching no docs/ file and retiring no vocabulary. Completeness was held to the item’s own named evidence rather than to the build, since nothing here is parsed by a test: all six delivered sites were read against the spec’s verbatim blocks and match them word for word, including the two Mermaid edges, the reviewer-rule replacement, the Findings, not fixes. paragraph, the conventions bullet, canonical-path step 2, the srp Verdict bullet and the two table rows, with the tree greppably free of any surviving invitation to edit the plan body. The one deviation from a verbatim block is disclosed in the item body and is right: the srp bullet’s specified "which is what the log exists to stop" ships as "the split", nothing in the delivered convention being called a log now that the conventions bullet says "the whole section dies". Two non-blocking notes: the spec body shipped with its verbatim blocks uncollapsed and no "shipped at <sha>`" note, deliberately and with a stated reason that held up in practice, since the specified words were the contract this gate compared against, but the In Review commit names a pre-rebase `2f51ca6 that resolves to nothing, so this entry is the only correct provenance record; and the emitted Spec-stage template tells the reviewer to append a round without naming the round’s internal shape, its Materials list omitting roadmap/workflow.adoc where the Implementation-stage template includes it, which is scope the spec did not name and is Backlog material.

  • R772 (b3d61b7 the delivery, e0bfffd In Review, post-Done follow-ups e920581 + 356c548; filed and Backlog → Spec 06f0abe, spec revisions 816d8ff + 2b57543 + f519dce + afaf1d7 + 3827c60, spec review findings d66ff23, Spec → Ready 0981559, Ready → In Progress cae9186): the dev loop’s live fact store has a door. A graphitron:dev session holds one store open for its whole run and every answer it gives comes out of it, but the store’s URL is private by design, reader()’s javadoc stating why (an in-memory name carries a UUID nothing outside the class can reproduce, and a caller recomputing a stamped path would be one edit from booting an empty store and reporting it as a schema with no facts), so a developer debugging a wrong answer had 219 relations behind that answer and no way to ask about them: the whole debugging surface was the MCP tools' fixed queries plus whatever a test could be written to assert. Now `GRAPHITRON_DEV_STORE_CONSOLE=true, or a <storeConsole><enabled> block, and the session prints a whole psql command; paste it and you have read-only SQL against the rows the running session is answering from, live as rounds land, with no change to how the session itself reads or writes the store. The mechanism is a second in-memory H2 database created MODE=PostgreSQL, holding one READONLY linked table per store relation with H2’s PostgreSQL server in front of it, and GraphitronModelStore.console(int) mints it beside reader() so the URL never leaves the class. Three constraints were measured rather than reasoned about and each is recorded in StoreConsole’s javadoc against the reader who would otherwise rediscover it: PostgreSQL mode is a creation-time property (SET MODE PostgreSQL` on a live connection does not retro-fit it, the first client connection dying with Schema "PG_CATALOG" not found), so putting the session’s own store in that mode would land under every generator query and under the codegen bootstrap that reads its model off exactly that boot, which is why the store’s mode is untouched and a test reads it back; pgjdbc cannot speak to H2’s PostgreSQL server at all, its startup queries including SET extra_float_digits = 2 which H2 rejects as a syntax error with no assumeMinServerVersion / preferQueryMode / options combination getting past it, which is why the surface is a command line rather than a JDBC URL and why the protocol pin shells out to psql; and psql’s backslash commands do not work against H2’s partial pg_catalog, so introspection goes through information_schema. 219 relations link in about a second and cost the store one pooled connection rather than 219, H2 pooling link connections per URL and user. The item’s one hard requirement is that the listener is on 127.0.0.1 and nowhere else, and it is checked rather than claimed, which is the finding the whole design is built around: H2’s "only local connections" is a peer check run after accepting, so the port is open to the network while H2 drops the session, and h2.bindAddress does confine the bind but H2 reads it once when its first class initialises, so the goal setting it at the top of execute() is necessary and not sufficient in a JVM where ModelCodegenDriver already opened a store. StoreConsole.open therefore probes every non-loopback IPv4 address of the host after starting the listener, requires every one to refuse and loopback to accept, and tears the whole console down through the same helper close() uses when any accepts, so the failure mode is a missing debug tool rather than an open port. Read-only means DML through a link is refused, which catches a mistyped UPDATE; it is not a sandbox, DDL on the console database is not refused and a connecting client is an H2 admin, and the docs and the test javadoc say so rather than implying a guarantee the mechanism cannot make. The port is ephemeral by default, which is the encouraged shape rather than a convenience: several dev sessions in one workspace is ordinary in this reactor, a fixed default would make the second session’s console fail on a port the first one holds, and a well-known port on a developer’s machine is the kind of listener found by something other than its owner. Both log arms carry a complete command, the enabled one straight from StoreConsole.connectCommand() rather than reassembled at the log site, and the disabled one, which is the default and therefore the line most developers meet, naming the command that starts a console rather than leaving that to the manual. Agents read the same coordinates as fields from a new store.console MCP tool, advertised on every boot in catalog.search’s shape so the disabled arm can hand back the enabling command, deliberately not folded into `status, its connectCommand the console’s own string from GraphitronMcpServer.ENABLE_STORE_CONSOLE and StoreConsole.coordinates() so the human’s line and the agent’s cannot become two spellings. Independent-session In Review → Done review: full reactor green under mvn install -Plocal-db, run twice, the second time after a rebase that brought R769’s per-thread graphitron-model store fixture onto the same FactStores file this item extended (BUILD SUCCESS both times, 14 modules, 6171 tests, with verify-roadmap-readme, check-adoc-tables, check-transient-citations, check-module-enumeration, check-schema-identifiers over 219 relations and check-adoc-xrefs over 203 anchored references all clean, the AsciiDoctor render and the javadoc reference gate passing); no code-string assertions on generated method bodies, the item generating nothing and its tests asserting log lines, the connect command, MCP structured fields and SQL counts; user-facing-doc check clean over all three touched .adoc files; retirement sweep skipped, the item declaring no retired vocabulary. Completeness was held to the item’s own named evidence rather than to the green build, and what mattered is that neither skippable test skipped itself into a false green: StoreConsoleTest ran 10 of 10 with none skipped, so the loopback-only case actually probed a real non-loopback address rather than passing on the trivial no-such-address arm, and StoreConsolePsqlTest ran 2 of 2, so the printed line was executed verbatim by a real libpq client rather than reconstructed from the handle’s fields, which is the one assertion that fails if the string a developer copies is wrong and the whole reason that test shells out. All ten StoreConsoleTest claims the spec enumerated are present, including the forced-verification-failure arm driven through a BindCheck seam, the connection-sharing count, and the store-mode read-back. Two beyond-spec additions were checked rather than accepted on their commit messages: the reactor-wide h2.bindAddress surefire property in the parent pom is necessary (the property has to arrive with the JVM and no graphitron-model fork can set it in time, test ordering deciding which fork loaded H2 first) and costs nothing else, verified by disassembling H2 2.4.240’s NetUtils, where getBindAddress is reached only from createServerSocketTry and getLocalAddress so client sockets are untouched, and by confirming StoreConsole holds the reactor’s only create*Server call; and the explicit console teardown on the MCP-bind-failure path out of execute() closes a real leak the spec’s cleanup() placement did not cover, pinned by a case that drives the whole of execute() rather than asserted in prose. The ambient routing bullet shipped terser than the spec’s draft, naming the tool and its purpose but not the fields or the disabled arm; the spec offered that fork and preferred it, the tool description carries the detail, and the ceiling stayed at 3,600 with the composed string measuring about 3,590. The manual row also correctly dropped the spec draft’s "the password it minted for this session", the password being fixed rather than minted, so the shipped prose is more accurate than the contract’s own draft. Three non-blocking notes: the spec body shipped uncollapsed with no "shipped at <sha>`" note, its four beyond-spec additions disclosed in the delivery and handoff commit messages rather than in the body, so this entry is the surviving provenance record; the spec’s "the coordinates reach `bindServer`" test claim ships as an assertion on `DevMojo.coordinatesOf with the threading itself compile-checked at its single call site, and the "console closes before the store" claim is deliberately unpinned with a stated reason, the store closing first still letting the console’s shutdown succeed so the ordering leaves no observable trace; and a <storeConsole><port> that will not parse throws out of execute() after sessionStore opened and before the shutdown hook is installed, leaving that store unclosed on the one path, which is the same shape as the pre-existing resolveDevDatabase dialect stop and matters only under a surviving mvnd daemon, so it is Backlog material rather than this gate’s business. Two commits then landed after the Done transition, so neither passed a gate and neither had a covering item, which is recorded here because the spec file is gone and a commit message would otherwise be their only trace. e920581 puts -X in the printed command: psql runs the developer’s ~/.psqlrc before anything else, and a startup file written for a real PostgreSQL server sets things this server does not have (application_name, bytea_output and their like are syntax errors to H2), so without it the prompt opens under a wall of red and every later answer carries the same errors ahead of it; the cost is that the session does not inherit the developer’s formatting preferences, and the manual says so and says to drop the -X to have it the other way. The same commit stopped the psql pin conflating the streams, which is what let a startup file fail the test rather than the console, so each case now runs psql under its own HOME and PSQLRC and sends stderr to a file rather than a second pipe that could deadlock against the read of stdout. 356c548 is javadoc only, recording on FactStores.perClass why the console’s cases did not move to R769’s per-thread funnel once it landed beside them: the reason is the surface rather than the lifetime, withSeededStore handing a body a DSLContext on purpose and a case that needs the store handle having nothing to ask it for. A later independent session checked both rather than accepting their commit messages: the reactor is green under mvn install -Plocal-db on the settled tree, StoreConsolePsqlTest runs 3 of 3 with none skipped so the new regression case is executed by a real libpq client against a real hostile startup file, connectCommand() remains the reactor’s only spelling of that command so the -X reached the log line and the MCP connectCommand field together and the two still cannot diverge, and the new case was proven to bite by removing the -X and watching it fail on exactly the documented H2 syntax error rather than on faith.

  • R785 (17d8154 the fork, 646bef5 the guard, 5e39131 In Review; filed 6f425dd, Backlog → Spec 7b66503): editing a manual page and rebuilding no longer kills the build. The build-time docs-index step embeds the public manual with an ONNX model whose tokenizer resolves its native library by System.load on a fixed cached path, exec:java ran it inside the Maven JVM behind a class loader exec-maven-plugin rebuilds per execution, and the JVM binds a loaded native library to one class loader until the process exits, so the second build that actually embedded died with UnsatisfiedLinkError: Native Library .../libtokenizers.so already loaded in another classloader. The content-hash stamp gate is what made it read as intermittent: an unchanged manual skips the embed and never touches the tokenizer, so the failure needed a stale stamp on a reused JVM, which is not exotic but is precisely the inner loop this repo documents, CLAUDE.md recommending mvnd whose daemon deliberately outlives a build and web sessions warming the full reactor before the developer types anything. The message named neither the docs nor the generator and suggested nothing the developer had done. The fix is one word of XML: the build-docs-index execution binds exec:exec with ${java.home}/bin/java, the classpath through <classpath/> and the main class as an argument, because the violated constraint is a property of a process and nothing inside the process can unbind a loaded library, so a boundary is the only place the invariant can be restored, at the cost of one JVM start against a 16-second embed that only runs when the manual changed. This extends a shape the tree already relied on rather than inventing one, the ONNX test running in a surefire fork and the tree-sitter path extracting to a fresh temp file per extraction and binding through FFM SymbolLookup; the docs-index step was the one native load with no boundary at all. Two things fell out: the fork owns its command line, so --enable-native-access=ALL-UNNAMED is now declared where it applies, the replaced pom comment having claimed surefire’s argLine covered this execution when that argLine configures test forks and this ran in the Maven JVM; and the generator’s main is an entry point again rather than a method invoked reflectively in a loader the plugin owns. Because reverting one word looks harmless and no single build shows the cost, the fix ships with a gate: NativeLoadIsolationCheck, invoked as check-native-load-isolation at verify beside check-coverage-agent-wiring and in the same shape, walking the root pom plus its declared modules, stating the invariant over the dependency rather than over this one execution so the next native dependency inherits the protection: a pom naming an artifact starting langchain4j-embeddings or onnxruntime must not bind an exec-maven-plugin execution on the in-process java goal. Dependency management counts as naming it, an execution the root pom binds being inherited by the marked module; a configuration-only block binds nothing and is not a violation; comments are stripped first; and the message names the module, the execution id and the consequence in the words the developer would otherwise meet cold. Parallelism is explicitly not the trigger and is recorded as such in the item body: the failing runs used -pl graphitron-mcp, one module and one execution, and CI has never seen it under -T 1C because every CI job is a fresh JVM. Independent-session In Review → Done review: full reactor green under mvnd install -Plocal-db (BUILD SUCCESS, 14 modules, no failing suites, 634 chunks at dimension 384 over 79 pages through exec:3.5.0:exec in phase, check-native-load-isolation and check-coverage-agent-wiring both reporting clean, NativeLoadIsolationCheckTest 14 of 14 with none skipped); no code-string assertions on generated method bodies, the delivered tests scanning pom XML; user-facing-doc check and retirement sweep both skip, the item touching no docs/ file and retiring no vocabulary. Completeness was held to the item’s own named evidence and then to the counterfactual the item could not itself run: with the fork in place, three consecutive stale-stamp embeds against one daemon each wrote 634 chunks at dimension 384 and a stamp-current run still printed the skip line, so the boundary did not cost the up-to-date check; with the pom flipped back to exec:java in that same daemon the first embed succeeded and the second died with the verbatim UnsatisfiedLinkError naming /root/.djl.ai/tokenizers/0.21.0-0.36.0-cpu-linux-x86_64/libtokenizers.so, and run_againstThisRepository_isClean errored with "native load runs in the Maven JVM" while it was flipped, so both the mechanism and the guard are known to bite rather than merely known to pass. One non-blocking note: the marker is a textual scan for the artifact name, so a module that acquired the stack purely transitively (by depending on graphitron-mcp) would not be marked, and the dependency quarantine confining langchain4j-embeddings to that one module is a pom-comment convention rather than a mechanical gate; the item’s approved wording states the marker rule in exactly these terms, so this is the contract’s boundary rather than a divergence from it, and closing it is Backlog material.

  • R769 (0120ad9 the delivery, b4e8f00 the rebase note; spec review 81c190e, author revision 9c7bd93, Spec → Ready 04ef40c, Ready → In Progress 557a231; the filing and Spec commits were rebased away and their SHAs no longer resolve on trunk): graphitron-model’s test suite stops applying the fact schema once per case and applies it once per test thread, clearing rows between bodies instead. 420 boots become 31, module test-class time 191.1 s to 65.2 s, and test execution wall clock 30.9 s to 11.6 s once Maven’s own 16.9 s of phases is held constant across both arms. `SeededStore.withSeededStore now delegates to a package-private ThreadConfinedStore beside it: a ThreadLocal boots a store on a thread’s first body and keeps it, never closing it, an in-memory H2 dying with the fork anyway. A clear is TRUNCATE over every base table outside the meta_ registry family and store_stamp, under SET REFERENTIAL_INTEGRITY FALSE restored in a finally, which is what makes the truncate legal rather than merely unchecked: H2 refuses to truncate a table a foreign key references but asks whether referential integrity is on before it declines, so the clear costs under a millisecond instead of a delete per table in key order. The flag goes back on because a case asserting a foreign key rejects a row has to keep failing when it should, and it should break at the reset rather than there. Re-deriving the materialization dependency edges is deliberately not part of a clear, MaterializeDependencies.populate reading the registry and the catalog’s stored view definitions and no fact relation, which is the settlement that makes this slice cheap; SeededStore.derive is the other derivation, does depend on fact rows, and is still called per case. The partition is derived from INFORMATION_SCHEMA once when the thread boots, which is what puts DDL out of bounds for a case on the funnel, a trap rather than a hole because a renamed base table makes the next clear name a relation H2 will not truncate. Two guards, both mutation-tested before being trusted: verifyBootState asserts at boot that every table the clear empties was empty on a freshly booted store, and verifyCleared asserts after every clear that every base table holds exactly its boot row count, scoped to the whole base-table set rather than to the clear’s own list, because a guard whose scope is the exclusion pattern’s output cannot see the exclusion being wrong. A nested call throws on an inUse flag that sits below the graph-anchoring overload’s delegation, so the whole graph-anchored half of the suite does not trip it. The boot-count pin is two counters, each owned by what it counts: ThreadConfinedStore.boots() against bootingThreads() as an equality, which is the confinement invariant itself, and a total on FactStores.boots() under a ceiling. The equality is pinned rather than the literal four because fixed.parallelism=4 sizes a ForkJoinPool that adds compensation threads when a task blocks, and this module’s own run boots eight stores on eight threads, so boots == 4 would fail today. No test class changed, which is the property that made this the right first slice: all 159 call sites across 30 classes adopted it by not changing, and no downstream module calls the funnel. One design fork the plan did not anticipate, disclosed in the item body and correct: the ceiling is enforced in ThreadConfinedStore.run on every funnel call rather than asserted in a test method, because a monotonic counter read from a test is read wherever the class scheduler happened to reach that class and under the randomised order the plan’s own acceptance asks for that position is nowhere in particular, and because FactStores is on four modules' test classpaths where the three modules this item defers boot per case in the hundreds by design, so the harness counts and states no policy while the budget sits next to the funnel whose claim it is. Also junit-platform.properties, whose paragraph argued the module’s concurrency safety on exactly the per-call store this change removes, now states the new reason (two classes on one thread do share a store, and what keeps them apart is the clear plus the guard) with its stale boot sighting deleted and all five junit.jupiter.* settings untouched; a HOMES entry in StoreFixtureGuardTest declaring the one fixture in the reactor that owns a store’s lifetime rather than handing it out; and RunawayRelation’s javadoc saying it is not usable inside the funnel and why. Independent-session In Review → Done review, approved on both gate questions. Full reactor green under `mvnd install -Plocal-db, run twice, the second time after a rebase that brought three unrelated items onto the tree (BUILD SUCCESS, 6178 tests, 0 failures, all six roadmap-tool gates clean and the javadoc reference gate passing); no code-string assertions on generated method bodies, the delivered test asserting store identity, row counts, exception messages and the two counters; user-facing-doc check skips, the item touching no docs/ file; retirement sweep skips, the item retiring no vocabulary. Completeness was held to the item’s own named evidence rather than to the build, and the evidence that mattered was that neither guard was vacuous, a previous round of this item having shipped a leak guard that could not fail. Both were made to fail independently at this gate: adding STORE_GRAPH to the clear’s exclusions produced "the clear did not put the store back into its booted state: STORE_GRAPH holds 1 rows where a booted store holds 0", which is the mutation that proves the round-two rescoping was load-bearing, a clear-list-scoped guard having no question to ask about a table the clear does not reach; and dropping the META_ exclusion produced "a freshly booted store already holds rows in [META_MATERIALIZE, META_MATERIALIZE_DEPENDENCY]", which independently reproduces the plan’s premise that a booted store holds rows in exactly those two relations plus store_stamp. ThreadConfinedStoreTest ran 7 of 7 with none skipped, so the equality pin and the budget assertion both actually executed, and the module ran green twice more under ClassOrderer$Random with the class order verified to differ between runs, 467 of 467 each time, which is the sequential-order dependence a shared store can introduce failing to appear. Two non-blocking notes. The plan body shipped uncollapsed and still says four out-of-scope classes and 19 surviving boots where the tree has five and 23, the correction living in the item’s Implementation notes rather than folded into the body, deliberately and under the findings-not-fixes convention this repo had just adopted, so this entry is the surviving record; and R768’s new passage pointed a reader at R769’s plan body for the reset’s shape, the guard’s scope and the two-part counter, which this gate deletes, so it was repointed at ThreadConfinedStore, verifyCleared and BOOT_BUDGET in the approval commit. One observation that is not a defect: the equality pin is an invariant true at all times, so reading it early is sound, but its detection power still depends on when its class runs, and a thread that booted twice only after the pin’s class had run is caught by the continuous budget check rather than by the equality, which is the same reasoning the implementer applied to the ceiling carried one step further.

  • R763 (ad5aad8 the delivery, 4e2583a the rework round; filed and Backlog → Spec a0f3646, research notes 479515c + 1d81023 + 3437ef8 + b732c78, Spec → Ready 1e3c082, Ready → In Progress 1202c45, In Review → Ready 0e4ed68): graphitron-sakila-example runs its test classes four at a time. It is the reactor’s terminal node, so everything it spends is build wall clock, and it was the one module running 800-odd tests one at a time while three of four cores sat idle; surefire:test on the module goes from 59-61 s to 44-50 s. Two defects stood in the way, both invisible until something ran the module’s classes concurrently. io.quarkus.test.junit.QuarkusTestExtension keeps its per-test bookkeeping in static single slots, so two @QuarkusTest classes in flight invoke one class’s method against the other’s instance, and parallel @QuarkusTest has never been supported upstream; the four classes whose subject really is the wire now share one @ResourceLock key carried by QuarkusTestLock, which makes them mutually exclusive while leaving them free to overlap the other sixty-odd, and QuarkusTestLockEnforcementTest fails the build when a new one arrives without it. TutorialSmokeTest was the fifth and needed no container: every assertion it makes is on GraphQL response shape, so it moved to the direct querydb harness, and its unscoped DELETE FROM film WHERE film_id > 5 became a delete of the one row the one writing case creates. The larger half was sixty-three assertion sites that only held while film or content held nothing but the seed, against eleven classes that write film and two that write content while they run: "the query returned five films" and "the film table holds only the seed" are different claims, and each case now makes the first, identifying the rows it is about or bounding its query where a count is genuinely the subject. Four fixture facts fell out of that and are recorded where they bite: film.rating carries DEFAULT 'G' so a G-filtered assertion counts other classes' rows and PG is the value the seed can own, film.length carries no default so a length bound excludes inserted rows, an approval file over an unfiltered root field is a claim about the database rather than the query, and four classes' email cleanup bands were open-ended and reached into each other. The method is the durable part: finding these by interleaving would have taken many runs, so the sweep ran a deterministic detector instead, inserting rows shaped like the ones the module’s own writers create and watching every table-dependent case fail on the spot. Its blind spot is the lesson, and cost this item a bounce: the first detector varied rating, length, release_year and the content parent but never the title, so every row it inserted sorted after the seed, no title-ordered page ever moved, and three cases in the filmsOrderedConnection title cluster survived to be caught at In Review by a single row titled A DETECTOR FILM. A detector’s coverage is the cross product of the columns it varies against the orderings the tests use, and a column left at its seeded value is a blind spot rather than a safe default. Also corrects a false sentence in tutorial page 4, which the old string-matching assertions could not see: the seed puts Mary, Patricia and Barbara in store 1, not the first three customers.

  • R796 (02f1795 the delivery, 1dbdd38 the rework round; filed and specced af45631, sync passes ebee6c4 + bb737f6 + 0e643f3, Spec → Ready f0b6ab0, Ready → In Progress 6b4475c, In Review → Ready d21986c, rework In Progress 1b864e1, In Review ec69740): the diagnostics drain no longer runs on the thread that triggered it. Workspace.enqueueAndNotify ran the recalculate listener inline, and that listener is the whole drain: walk every queued file, one read on the session-wide reader bounded only by the 30 s session budget, publish per file. For didOpen that thread is the single one lsp4j reads inbound messages on, so a drain there did not merely delay diagnostics, it stopped the server reading its own input; every queued request waited behind it, $/cancelRequest included, so the client’s escape hatch was unreachable exactly when it was needed, and an editor issuing any request synchronously froze its own UI. That is what a developer reports as the editor hanging rather than as diagnostics being late, and it was reported against a real emacs session before roadmap/lsp-surface-latency-budgets.md reproduced it in a harness at 31310 ms via didOpen against a median of 4 to 20 ms for every other surface. markAllForRecalculation occupied the dev loop’s watcher thread the same way. The fix is one injected Executor on GraphitronTextDocumentService, single-threaded rather than pooled and for a correctness reason rather than a frugal one: two drains would serialize inside the one-connection reader anyway while each held a walk’s worth of snapshots, and their per-file publications could interleave so the client ended on the older of two answers. DevServer and Launcher each mint a named daemon thread, graphitron-lsp-diagnostics-drain, so a stack dump of a stuck session says where the drain is. Three properties carry the asynchrony and each is pinned by an ordering assertion rather than a duration: an AtomicBoolean collapses N submits during one drain into one follow-up, cleared before the queue is read so a mutation landing mid-drain gets a fresh one; Workspace.holdsViewFor makes the publish loop skip a URI the workspace no longer holds a view for, which closes the one hazard asynchrony genuinely introduces, a didClose landing between the drain’s walk and its publish and its empty-list clear then being undone by the drain’s stale list; and the three existing harnesses that assert a publish synchronously after didOpen returns pass Runnable::run and keep their assertions verbatim, which is what injecting the executor buys over hard-coding one. What the item explicitly does not fix is the drain being slow, which is roadmap/diagnostics-drain-overruns-its-session-budget.md; this removes a structural hazard that outlives any single slow query, a drain on a large workspace always being able to grow past a keystroke’s patience. Independent-session In Review → Done review across two rounds. Round 1 sent it back on a new failure mode the change introduced rather than on anything it failed to do: DevServer.serve’s `finally shuts the drain executor down per connection, nothing clears Workspace’s single listener slot, so between an editor detaching and the next connection’s `setClient a build swap still reached the dead connection’s service and the submit was rejected. Reproduced rather than reasoned, RejectedExecutionException leaving enqueueAndNotify onto a Maven thread, and DevMojo made it worse rather than absorbing it: regenerate logged "catalog refresh after save failed; keeping previous" for a refresh that had in fact succeeded, then threw a second time out of an inner catch (RuntimeException) past an outer catch that only handles MojoExecutionException into a DebounceExecutor task whose ScheduledFuture nobody inspects, and rebuildCatalog called the mutator before its recompile so a classpath change with no editor attached lost the recompile and reported a failure that did not happen. The comparison is the point: inline, that window was quiet, lsp4j’s RemoteEndpoint.notify catching its own write failure and logging at INFO, so a mutator must not gain a throw the inline path never had. The rework absorbs the rejection at the submit, resets the collapse flag so a service surviving one rejection is not wedged out of ever submitting again, and logs at debug; both halves were mutation-tested at the gate, removing the reset and removing the catch each failing rejectedSubmitIsAbsorbedAndDoesNotWedgeTheFlag on its own assertion, and round 1’s original reproduction against a genuinely shut-down ThreadPoolExecutor now returns normally from both mutators. Round 1’s second finding was a contributor-facing doc teaching the inverse of the new code: docs/architecture/how-to/dev-loop-internals.adoc’s trace-reading table still read a large `workspace.notify as "the mutation’s real cost is the diagnostic recalculation it triggers" when after this change notify is a flag-and-submit and a large one means the listener has stopped being one, and its thread= row still used the drain as its motivating example of a notification blocking the reader when the drain is now exempt by construction. Both rows now teach the post-change reading and agree with enqueueAndNotify’s own javadoc. Neither mechanical gate could have caught that: the item declares no retired vocabulary and the delivery touched no `docs/ file. Full reactor green under mvnd install -Plocal-db on the rebased tree, DiagnosticsDrainThreadingTest 4 of 4 with none skipped; no code-string assertions on generated method bodies; user-facing-doc check run over the one contributor-facing page touched and clean of roadmap-internal markers, retirement sweep skipped. One non-blocking observation from round 1 outlived the item and was filed as R798 rather than widened into it after approval: markAllForRecalculation still calls loadVocabulary inline, so a build swap can still spend the session budget on the watcher thread, and because that call sits outside both the mutate and notify spans the trace the rework just corrected attributes the time to no span at all.

  • R792 (b7f5026 the delivery, 4fa6c12 In Review; filed and specced e5363d4, sync passes ebee6c4 + 0e643f3, Spec → Ready 7646664, Ready → In Progress af4dafc): the language server’s out-of-budget warning names the read instead of dumping the statement. StoreAccess.warned logged the aborted SQL as the WARN’s payload, and the statement a whole-workspace diagnostics drain issues is one nested select per relation it reads, so the line ran to thousands of characters, pushed everything around it out of a dev console, and still did not say which question the server had given up on: nothing to name in a bug report, nothing to say out loud, nothing to grep for the next time the same surface went quiet. A new StoreRead enum beside StoreAccess declares the vocabulary, one constant per store-reading surface and seven as the tree stands (HOVER, DEFINITION, COMPLETION, INLAY_HINTS, CODE_ACTIONS, DIAGNOSTICS, DIRECTIVE_VOCABULARY), each carrying the phrase the warning speaks, and all three of StoreAccess’s doors plus the two `Workspace facades in front of them now take one. R795 landed a fourth door, annotating, while this gate was open, and it takes a StoreRead like the rest, which is the design decision holding rather than being re-argued: the spec had reasoned that a new reader changes what a constant is answered by and not what it is called, so a door added afterwards is not a change to the vocabulary. An enum rather than a free-text string because the vocabulary is the deliverable: a reader sees the whole set in one place, a new surface must add to it rather than invent a spelling, a test asserts against a constant instead of a sentence, and a renamed surface leaves an unused constant rather than a lying log line. Deliberately independent of the door choice, which is about latency contracts, so a future interactive caller of the bulk door is not a change to the vocabulary. The WARN keeps its posture sentence, the one thing that tells a developer nothing they see is wrong, and gains a pointer to where the statement went, spelling no.sikt.graphitron.lsp.state.StoreAccess out because that is what a developer types into a logback config; the statement itself drops to one DEBUG line on that same logger under the same read’s name. No posture changed, StoreAnswer.OutOfBudget keeps both components, the budgets are untouched, and the MCP server’s out-of-budget error is deliberately left alone, being a tool result an agent reads as its whole answer rather than a console line a human scans past. The user manual’s "a surface goes quiet after a build" entry was corrected in the same commit, its old "the warning names the statement" sentence having become false, and now tells the reader to enable that logger if a report needs the statement. Independent-session In Review → Done review: full reactor green under mvnd install -Plocal-db (BUILD SUCCESS, 14 modules, lsp.StoreOutOfBudgetTest 5 of 5 with none skipped); no code-string assertions on generated method bodies, the delivered case asserting captured log events; user-facing-doc check run over the one manual page touched and clean of roadmap-internal markers; retirement sweep skipped, the item declaring no retired vocabulary. Completeness was held to the item’s own named evidence rather than to the build, and that evidence was mutation-tested at the gate rather than accepted for passing: theWarningNamesTheReadAndTheStatementDropsToDebug provokes a real overrun through RunawayRelation on store_graph_source and captures the boundary’s own logger with a logback ListAppender, and putting the statement back into the WARN fails it on the no-statement assertion while replacing the constant’s phrase with a fixed string fails it on the name assertion, so both halves of the behavioural claim are known to bite. All seven constants are wired at a live call site and StoreAccess is the module’s only store read, so the vocabulary is complete for the tree it claims to cover. One non-blocking note: only DIAGNOSTICS is exercised end to end, so a call site passing a wrong constant would log a plausible wrong name with no test to catch it; the constant sits at the door where the mismatch is visible in the diff, and the item’s approved verification bounded itself to the log behaviour, so closing that is Backlog material rather than this gate’s business.

  • R795 (480bb20 the four steps, 18cea5e the door enforcer, 4396dfc the rework round, ea3f529 the ceiling lesson; filed 1d7f91c, Backlog → Spec 83360d9, sync passes ebee6c4 + 0e643f3, spec review 2edae5f, spec revision 39cb1d2, Spec → Ready 347b798, Ready → In Progress 19cef69, In Review dd575e7, In Review → Ready aabf6c8): no language-server surface blocks the editor. Jump-to-definition stalled over a second on certain SDL declaration names, and measuring all six store-reading surfaces rather than the one reported is what found that the reported surface was not the worst one. The defect is one arm of eleven in the single statement DeclarationFacts builds: redirectArm joined the catalog census sql_table to the derived intent_type_backing on the class name, H2 was free to pick the driving side and picked the census, so the view was evaluated once per catalog table with the two predicates that make it cheap applied after that expansion, and the arm spent 1.1 s of the read’s 1.3 s establishing that it had nothing to say. Seven controls are recorded because three refuted a candidate fix: the relation alone is 22 ms and the census alone 8 ms, a derived table around the filtered relation is inlined, an IN subquery is evaluated per driving row just the same, and only a correlated lookup, which gives the planner no driving side to choose, comes back at 20 ms. So the arm now selects from the filtered backing relation and carries the census as a nested multiset correlated on the class name, flattened to its real grain of one (backing class, bound table) pair, with CENSUS_ORDER restoring in Java the schema-then-table order the flat join stated in SQL; the arm goes 1098 ms to 21 ms and the goto-definition it dominated about 1300 ms to 45 ms, returning the same rows in the same order, and the declaration-name hover that shares those arms comes with it. The scaling dimension is stated because nothing measured it: the cost tracks the consumer’s database catalog, not their class census, which a re-measurement against 1096 real classes and 4073 methods established by moving the number not at all. Step 3 is the half no single surface’s own cost explains. Inlay hints are configuration-gated and InlayHintConfig.defaults() has every axis off, so the probe’s 10 ms was an early return; with every axis on one fifty-line request over the sakila schema takes 11918 ms, four times the interactive budget, which inverted the item’s picture of which surface is worst and raised a defect this item does not fix (R799). Before the split that request held the interactive reader for the full three seconds and was then aborted, so every hover and jump queued behind it waited three seconds for a request that produced nothing. The earlier draft’s answer was to tighten INTERACTIVE_READ_BUDGET, and the Spec review refused it out of that constant’s own javadoc: the low-seconds figure is deliberately not a latency policy, a budget miss is not a slow answer but no answer, and a threshold tight enough to police slowness drops correct answers on the loaded machine a dev session runs on. So the budget is untouched at 3 s and 30 s, latency reporting stays with LspTrace’s 100 ms slow-span tag, and what splits is the door: `StoreAccess gains a third reader and a fourth door, annotating, which inlayHint alone goes through, carrying the interactive budget rather than a constant of its own precisely because a second constant holding the same number would invite tuning it into the policy the budgets refuse to be. The split is by who is waiting, not by what budget they wait under: a hint arriving late is invisible where a cursor arriving late is not. Step 4 is SurfaceScanCountTest, a new instrument in the tree, reading the scanCount H2 reports under EXPLAIN ANALYZE over all six surfaces driven through the same provider seams the statement-count enforcers use. Scan counts rather than wall clock because the tiers refuse to fail for slowness, and the existing counts cannot see this defect at all: DeclarationDefinitionStatementCountTest pins definition at one statement per request and that one statement is the bug. Ceilings are the broad net and theCensusLookupDoesNotTrackTheSchemasSize is the sharp assertion, measuring what reaching the census costs over reading its driving relation alone at two schema sizes so the driving view’s own cost, which this item does not own, is excluded: 184 and 3724 scans as a join, 63 and 63 as a lookup, the census’s own row count once. Independent-session In Review → Done review across two rounds. Round 1 approved the code and sent the item back on its documentation: docs/architecture/how-to/dev-loop-internals.adoc still said the session mints three readers with two behind StoreAccess, enumerated the doors without annotating, and gave the reason for the split as "per latency contract rather than per consumer", which is the argument step 3 exists to replace, so a contributor applying the documented principle would conclude no third reader was warranted and route the next document-scoped surface onto the cursor’s reader. Neither mechanical gate could catch it: the item declares no retired vocabulary and the delivery touched no docs/ file. The rework rewrote that paragraph to four readers with three behind StoreAccess, annotating named for what it is for, and the principle restated as who is blocked on the answer. It also closed the round’s most valuable non-blocking note as a real gap: nothing pinned that the service routes inlay through annotating, so a one-line edit at the call site would have put a whole-region read back in front of the cursor with every test green, and theInlayRequestIsRoutedThroughTheAnnotationDoor now drives the service and reads which reader answered off the boundary’s own warning, whose budget is the reader’s, turning on an arm rather than a clock. Verification at the gate went past the green build to the item’s own named evidence, mutation-tested rather than accepted for passing: with redirectArm reverted to the flat join both SurfaceScanCountTest cases fail, the sharp one at the smaller of its two schema sizes, which is what makes the small fixture defensible; the four affected LSP suites were also re-run after rebasing onto R793’s meta_materialize registrations, since that change moves the counts the ceilings measure. Full reactor green under mvnd install -Plocal-db; no code-string assertions on generated method bodies, the delivered cases asserting scan counts, captured log events and session settings; user-facing-doc check run over the one contributor-facing page touched and clean of roadmap-internal markers; retirement sweep skipped, the item declaring none and the budget constants keeping both their names and their number. One residue was filed as R802 rather than folded in after approval: the ceiling javadoc gained a paragraph claiming every ceiling was confirmed to fail with the defect reinstated while the paragraph beneath it still says the ceilings would not have caught it, and the accurate version of that claim was the corrected step 4 of the item file this entry retires. Only DeclarationDefinitions and DeclarationHovers read DeclarationFacts, so four of the seven ceilings were never mutation-tested against this defect, and the sequential assertions mask each other besides.

  • R805 (712f272 the delivery, a310024 the three gates landed ahead of the move; filed 1ba9567, fork resolved 4f1e84c, Backlog → Spec 80eb452, consult folded 90f05a4, Spec → Ready 5b92d57, Ready → In Progress 27e98ed, In Review 74ad4ae): docs/architecture/ grows a principles/ section beside the three Diataxis quadrants, and development-principles.adoc moves into it keeping its filename, the move byte-exact except its six explanation-sibling xrefs repointing to ../explanation/. The section index routes to three destinations (the moved axioms, the strategic principles staying at the docs root with the register-and-audience reason stated in place, the fact model staying in explanation/ with its reference web) and carries no axiom roster and no count, so a seventh axiom falsifies nothing; architecture/index.adoc introduces principles as a lead-in band above the untouched 2x2 grid, since principles govern all three quadrants, which is exactly why they are not one of them. Link surgery ran by mechanism rather than by grep-and-hope: the ARCH_QUADRANT entry flips to principles and the Main.java emit sites follow, the anchored xrefs from typed-rejection and dispatch-axes become cross-directory, the concept page’s authored href repoints, the prose paths in CLAUDE.md, the srp and reviewer-prompt skills, the principles-architect agent and the path-qualified javadoc mentions repoint, and roadmap bodies got the mechanical sed. The three gates landed additive-then-cutover, green over the unmoved tree first, so their scope was not tuned to the move they validate: AdocXrefAnchorCheck widens to unanchored xref: targets with the path verdict a function of source provenance (build failure for pages authored under docs/ and the generated status boards, report-only for roadmap prose, the population its self-reporting argument was written for) plus a 1000-reference anti-vacuity floor wired in docs/pom.xml; ArchQuadrantBindingTest binds roadmap-tool’s private copy of the docs layout to the tree so the next move fails the build instead of shipping a live 404; HowToIndexCoverageTest parameterizes its bidirectional rule over the five section directories, the gate that proves the section real rather than the move complete. Independent-session In Review → Done review: full reactor green under mvnd install -Plocal-db (BUILD SUCCESS, sakila-example 843 tests with 0 failures, the widened gate collecting 1960 cross-file references with every path staged and every anchor explicit), the move verified byte-exact by diffing the delivered page against the pre-move blob, the rendered principles pages checked to carry the stylesheet and site nav that the hand-maintained docs/pom.xml roster silently drops when missed, retirement sweep for the old path clean over every prose surface, user-facing-doc check clean (the only roadmap-internal markers on touched docs lines were pre-existing prose whose xref path changed), no code-string assertions on generated method bodies. The durable fix the roster deferral points at, deriving the css/docinfo staging fan-out from the staged tree, is filed as its own Backlog item (derive-docs-staging-asset-fanout).

  • R804 (267cde8 the delivery; filed 1ba9567, corrections taken back from R803’s revision a4171d5, principles consult folded c2a5bd3, Backlog → Spec 64d356f, spec review ea76bbf, spec revision af314db, Spec → Ready ddd0cce, In Progress → In Review 267cde8): the rule governing what a single fact column may hold is stated on the two explanation pages, so the next author reaching for LISTAGG meets an argument rather than only a gate. R803 had already converted the serialized-collection columns to rows and landed CollectionValuedColumnGateTest; this is the prose half, and its whole value is the discriminator, because the first framing the item was filed under, rendering being a query-time construct, is false of the store as shipped: it legitimately holds renders, the rendered coordinate and the case folds among them, and the worst offender defended itself as a grouping key rather than as presentation. What replaced it is two clauses. A column may carry an opaque value the store did not compose, a captured message or a transcribed docstring, provided nothing joins, groups or filters on it, which is the permission seven column comments already spell in the same words, "display material, never a dimension"; a column anything does join, group or filter on must be atomic to the engine and a function of its relation’s own key, and what fails that is a collection inside a scalar, delimiter or no delimiter, which is also what makes the gate’s denylist coherent about ARRAY_AGG. intent_type_backing_conflict carries both verdicts under one key: candidates passes, the same set joined into one string would not, and the contesting classes are rows on intent_type_backing under that key. The argument is capability rather than taste, and it is lifted from the two live sites that already make it rather than minted: a serialized set answers equality of the whole set and nothing else, where the rows answer that and membership too, for one join. Two scoping sentences carry the rule to where it is reached from: the "name the row" one-sentence check now has to survive at the column grain, and the strata roster’s diagnostic placement exemption is scoped to that relation’s name and population, with coordinate passing on its own comment’s argument that its atoms ride the same row and the retired path-truncation column failing on the same relation. naming-the-row.adoc gains the failing sibling of the intent_bound_table candidate count, in the page’s own pantry register. The Spec gate is what makes the concrete history in the paragraph trustworthy: it caught the membership cost attached to the wrong retired column, a set of classes said to have been filtered by a directive, and the resolution moved that cost to the relation that actually paid it, the claim-conflict relation’s serialized directives and the MCP facet that offered them as a filterable dimension, so asking for the conflicts involving one directive returned only the conflicts whose entire set was that directive. Independent-session In Review → Done review: full reactor green under mvnd install -Plocal-db (BUILD SUCCESS, 14 modules, none skipped, CollectionValuedColumnGateTest 2 of 2). Completeness was held to the item’s own named evidence, which for a prose item is that every checkable claim is true of the tree, so each was re-checked rather than read: the seven "display material" sites are all column comments, intent_type_backing_conflict.candidates, intent_type_backing and diagnostic.coordinate are live and their own comments carry the arguments the paragraphs attribute to them, intent_authored_claim_conflict no longer carries the directives and its comment says why in the same terms, the gate exists in graphitron-model and its javadoc discloses the gap and the path-truncation exemplar exactly as the enforcer line reports, and no retired identifier appears on either page. The same-page cross reference is the one claim no build gate covers, since AdocXrefAnchorCheck scans cross-file xref: only, so it was checked in the rendered HTML: href="#provenance-pick-the-shape-per-fact" against a heading publishing that id, with no unresolved-reference fallback text on the page, and the target section does make the argument the sentence attributes to it. User-facing-doc check clean over both pages, no roadmap-internal markers; retirement sweep skipped, the item declaring no retired vocabulary; no code-string assertions on generated method bodies, the delivery adding no tests at all.

  • R811 (c5d5da2 the delivery; filed 73242e1, Backlog → Spec b455425, spec correction 50cffd2, spec review round 1 2ae532a, spec revision ab33377, spec review round 2 7aea6bd, reviewer’s round-2 tweak 46abd93, author’s draft-reading bcdcdd2, Spec → Ready 4a576bf, Ready → In Progress e276f4d, In Progress → In Review f085c77; Done-gate review in this commit): a meta_materialize registration that makes some other relation’s read more expensive now fails the build instead of landing unremarked. The gap it closes is that a registration is bought for one reader and changes the plan of every relation naming its target, and nothing priced that: report-inline-multiplicity ranks breadth and says breadth is not cost, SurfaceScanCountTest holds ceilings over reader surfaces and none over a relation, and MaterializeRegistryGateTest asks whether a registration is well formed and not what it costs anybody. DerivedReadCostTest prices every pair of a registration and a relation whose derivation reaches its target, in both shapes, and asserts four things: the domain’s size on both axes, the direction itself, the cells whose unregistered side could not answer, and that such a cell is recorded rather than failed. Both axes come off the booted store, registrations from the register and readers from MaterializeDependencies.registrationsReachedByView, which is the walk that already derives the refresh order asked the other way round, one memoized walk so the two answers cannot disagree about what reads what. UnregisteredRelation is the instrument, RunawayRelation’s rename-then-create sibling taking a `Registration rather than a name so it cannot be pointed at an unregistered relation; its contract is the session, H2 resolving a view’s table references once per connection and keeping them, so the writer surface that installs the swap is precisely the session that cannot see it and a measurement goes through a minted reader. The claim carries no number, so there is nothing in it to raise: a deliberately accepted pair is a row in an equality-pinned set, and the day the lever lands the row has to go. intent_node_id_decode gains the cost warning its own comment owed, and fact-model.adoc gains the rule beside the depth rule it qualifies, that the cost a registration is admitted on is not only what it saves its own reader but what it does to the others. Two results reversed what the item believed. The control refuted its own leading suspect: un-registering intent_resolved_type_binding does not make the decode family cheaper by anything worth naming, and the two large findings belong instead to intent_field_reference_step_hop, the registration this item was found alongside and had credited with recovering most of the decode’s gap, which costs intent_field_reference_step_target 32x and intent_field_column_scope_live 10x, both growing with the schema. And the fixture the spec specified would have measured nothing: a scaled schema of @table-bound types carrying one scalar field leaves four of seven targets and most readers empty, and over an empty relation the comparison sees only H2 charging a table visit one scan per naming where a short-circuiting view is charged none, so the gate would have passed while seeing nothing, which is the exact failure the spec warned about without noticing its own fixture was an instance of it. Nodehood was the missing declaration. Both findings are the reason the three registrations now under suspicion are filed as R815 with their figures rather than acted on here, the spec having put the lever out of scope from the start. What this item did not deliver is the other half of its own title. The decode’s ten-times move is still unattributed, and the honest reason is that the two trees carrying the 5.5-second and 50-second figures are gone from the repository, so the per-commit control the spec described has nothing to run against; the spec recorded those hashes as unresolvable two review rounds before the implementation started and promised the attribution anyway, which is an author defect the implementation exposed rather than a shortfall in it. It is recorded here as abandoned rather than filed as a successor, because it is not answerable by anyone: what replaced it is a gate that fails the next such change at the moment it lands, which is a better answer to the question the item was filed for than the retrospective one would have been. Independent-session In Review → Done review, by the spec’s author, the guard disqualifying only the implementer: full reactor green under mvnd install -Plocal-db (BUILD SUCCESS, 14 modules, DerivedReadCostTest 4 of 4 in 50 s, UnregisteredRelationTest 1 of 1, MaterializationOrderTest 9 of 9). Completeness was checked rather than read: the ratchet was mutation-tested at the gate by deleting the binding pair from KNOWN_NON_MONOTONIC, which fails aRegistrationDoesNotCostAnotherRelationMoreThanItSaves with the unexpected-element message, and the file was restored. Two disclosed deviations from the spec’s Acceptance list were accepted rather than bounced, both stated in the delivery and neither silent: the attribution above, and the pass-on-exhaustion arm firing through RunawayRelation rather than on a real domain cell, no cell exhausting at this fixture size because the walk the register describes as not finishing in two minutes does finish here, which meets that criterion’s intent (the arm shown firing through the real read path) while differing from its letter. The instrument’s own case pins that the swap preserves both the relation’s answer and a dependent view’s, which is what every figure taken through it rests on. User-facing-doc check clean, the only docs change being two contributor-facing paragraphs on fact-model.adoc with no roadmap-internal markers; retirement sweep skipped, the item declaring no retired vocabulary; no code-string assertions on generated method bodies, the delivery adding no emitter tests.

  • R826 (1491f9a the model DDL, 60d01fe the capture registration, 93d2ee8 the execution-tier fix, 8b5b23d the roster, re-price and re-measurement, 6c94642 and 940e05d the two later re-pins, adf6cfc the rework round; filed 105a2cc, Backlog → Spec 6212628, Spec → Ready 3bb36a2, Ready → In Progress cceeae3, plan collapsed 3e373f6, In Progress → In Review 39d9583, In Review → Ready 1f7684f, In Progress → In Review 7d5811b; Done-gate review in this commit): intent_node_id_instruction is a materialized target rather than a view, so the rule is evaluated once per capture instead of four times per read, and a consumer whose schema is large enough gets a build that finishes. The rule is named by three view bodies and the decode slot reaches it through a local alias driving both its union arms, so one read of the slot expands to two whole evaluations; the rule’s own cost is internal, in the TARGET_TABLE_NODE_TYPE arm that joins instructed to slot_table derived from it, and H2 inlining a non-recursive WITH exactly like a view recomputes that inner alias once per driving row. Nothing a consumer authors changes: the canonical name every reader already spells is the name the target table takes and the renamed rule is the new _live view, so the item declares no retired vocabulary and means it. The fix had been stranded on quickfix/10.0.0-RC34 since RC34, 105 trunk commits past its merge base, and the port was four steps of which only the first was the branch’s: land the three commits with both conflicts resolved keep-both, roster the index exemption, re-price the read-cost matrix, and re-measure the registration comment on the tree it lands in. Two of the three later steps found things the branch could not have known. The index exemption is a different kind of argument from the four already rostered, which each declined a measured lever: this target has no candidate to measure, because all three readers name it in their own driving FROM and join outward, so nothing probes in and no coordinate exists for an index to serve. That follows from what the relation is, the population those three views each partition, rather than from a shape somebody chose, and the roster’s javadoc now separates the two kinds so a row of the second is falsified by a new reader rather than by a new figure. The read-cost matrix then moved the opposite way from the Spec’s prediction: CELLS fell by five instead of growing, because the reachability walk records a registration when it meets its target and stops rather than descending, so registering a relation cuts every reader’s reach at it. Three pinned non-monotonic rows charged to intent_argument_scope_table went with it, and the reason matters more than the drop: none of intent_node_id_encode, intent_node_id_decode_slot or intent_node_id_decode_defect names the scope table itself, all three reached it through the instruction rule, so they did not get cheaper, they stopped reaching, and the cost moved to the refresh, which is a view in the same domain holding its own cell against that registration monotonically. intent_node_id_decode_endpoint names the scope table directly, kept its cell, and was never pinned, which is the control on that reading. The Done gate bounced one round. The re-measured comment retracted the branch’s claim that this was the registry’s most expensive refresh, correctly, but replaced it with "fifth of this registry’s ten" against a registry that already held eleven at the commit that wrote the sentence, intent_argument_column_scope_live having landed on trunk before this item started work; the two re-pins after it revisited DerivedReadCostTest and neither revisited the SQL. The ordinal was the half that blocked, being unverified rather than merely stale, since nothing had weighed that refresh against this one’s 1695 scans. The rework replaced the place with four names, intent_field_reference_step_hop_live, intent_field_column_scope_live, intent_carrier_data_field_live and intent_node_id_decode_hop_column_live, on the ground that a place is falsified by any registration landing beside it and a name is not, which is the lever this item’s own roster javadoc had stated one paragraph away. Independent-session In Review → Done review, the reviewer being the Spec gate’s reviewer and differing from the implementer: full reactor green under mvnd install -Plocal-db (BUILD SUCCESS, 14 modules, none skipped) on the reworked tree. Completeness was checked against the tree rather than read off the plan: the three quickfix commits landed with their DDL hunks byte-identical to 8c56f05 apart from the single terminator the keep-both resolution moves, the four named registrations each resolve exactly once in the register, the reach argument was verified by reading the bodies (intent_node_id_instruction_live names intent_argument_scope_table three times where the three former readers name it nowhere), KNOWN_NON_MONOTONIC’s "two mechanisms, the first three, the last four" matches the seven rows it now holds, and the retirement sweep is clean with every canonical-name site surviving as the `Retired vocabulary section declares. No new test, as the Spec approved: the claim was already gated, and the item’s own work was the re-pinning those gates forced.

  • R784 (835013f the decode-slot lift, dfaf5ee the per-column partition and the obligation-consuming emitters, ba40389 fixture, schema, coverage and docs, f2bad1b In Progress → In Review; filed efbe7f8, Backlog → Spec 0b75a55, Spec → Ready 3f1bfc3; Done-gate review in this commit): a cross-table @nodeId reference whose lifted foreign-key columns straddle the matched key now partitions per column on UPDATE instead of rejecting. Its out-of-key columns are SET writes, its in-key columns stay identity, supplying the WHERE predicate where nothing else does and otherwise contributing only an agreement obligation checked before any DML runs. The shape has no workaround that preserves the global ID, and Graphitron 9 generated exactly the wanted statement, so the rejection was a regression against a real consumer schema rather than a guard. The bulk of the work was not deleting the reject. Every emit site inferred a decoded composite’s record slot from the column’s position in its partition group, an invariant that holds only while a carrier lands in one partition whole; partitioning per column falsifies it, and the failure mode is silent, writing one decoded key column’s value into another. Stage 1 therefore carried the slot as an explicit datum before any behaviour changed: ColumnOverlap’s load-bearing-invariant prose is deleted in favour of a `SlotColumn(slot, column) pairing every adapter must state, and SetColumn, KeyColumn, MapBinding and RecordBinding all carry a slot the readers emit as value<slot + 1>(). The fixture is built to catch exactly that: catalogue_item’s foreign key to `catalogue lifts (tenant_id, catalog_code), so the straddler’s single SET column sits at the second decode slot and positional inference would write the tenant id into the catalogue code. Two structural decisions carried their weight. AgreementObligation is a component on the UpdateRows carrier rather than a per-emitter derivation, because the carrier has four emit consumers and only two of them intersected the partitions for themselves; the other two, buildCarrierUpdateChainSingle and buildCarrierBulkPerRowUpdateBody, turned out to ship with no cross-partition agreement check at all, so making the fact a stated one closed a live gap rather than adding a new case. And the nullable straddler is a build-time reject on its own permit, NullableStraddlingReference, not a widened MixedCarrierKeyMembership: an explicit null would write NULL into the out-of-key half and leave the in-key half populated, which MATCH SIMPLE accepts as a satisfied foreign key, and "don’t straddle your own key" and "make this reference non-null" are different fixes for tooling switching on lspCode(). MixedCarrierKeyMembership survives, narrowed to the own-columns carrier, where writing half the columns means moving the row. Two deviations from the Spec, both disclosed in the delivery rather than silent, and one of them better than what it replaced. The Spec’s fix for the fused keyGroupsOf discriminator would have moved arity-1 decoded key columns between the DSL.val-wrapped and unwrapped emit shapes, polluting stage 1’s own byte-identity gate and destroying its ability to distinguish a slot-threading bug from the reshuffle; carrying decodeSlot on MapBinding instead removes the same defect at four readers rather than one, with every existing binding at slot 0 so the bytes are unchanged. render/LookupRows.slotValueExpr needed no change at all, already reading an explicit index. Independent-session In Review → Done review, the reviewer sharing no session with any implementation commit: full reactor green under mvn install -Plocal-db against a private repo head and a per-worktree database. Completeness was checked rather than read off the plan. DmlSqlBaselineTest pins the contract’s emitted statement literally, one SET column with each key column named once on the single-row arm and a v(...) alias naming each column exactly once on the bulk arm, which is the shape that would not merely be redundant but would fail to run if a contributor doubled; StraddlingReferenceUpdateExecutionTest proves the same-tenant re-point writes only catalog_code and does not move the row, and that a cross-tenant input throws with nothing written on both arms; MutationDmlNodeIdClassificationTest asserts partition, slot and obligation on all four carriers, which is what makes a consumer unable to drop the component silently; and the untouched SelfFkNodeIdUpdateExecutionTest and BulkUpdateSetAgreementExecutionTest stayed green, which is the standing regression evidence that the obligation rewrite reproduces the self-FK emitters' previous output. One build failure was investigated and dismissed: CatalogRefreshTest, a 1.6-second latch around a debounced filesystem watcher in a module this item does not touch, failed under contention from a concurrent reactor build and passed on a clean re-run, but because that failure had skipped graphitron-sakila-example the reactor was resumed so the execution tier and the SQL baselines actually ran before approval. User-facing-doc check clean; retirement sweep skipped, the item declaring no retired vocabulary and narrowing an arm rather than retiring it; no code-string assertions on generated method bodies, the new pins being emitted SQL and executed behaviour. One non-blocking finding is filed as R829 rather than bounced: the payload-returning arms' new agreement preamble is never emitted with a non-empty obligation list anywhere in the corpus, so the gap this item closed there is carrier-verified but unexercised.

  • R726 (f79d83ee the delivery and the amendment; filed eb3e6ffae, Backlog → Spec 648df10a, Spec → Ready 1f961b5a, Ready → In Progress 5bd0cdee, In Progress → In Review e6c2aa6c; Done-gate review in this commit): what intent_node_id_instruction does at the multitable coordinate is stated on the relation itself and pinned at both tiers. This closes the 2026-08-20 nodeId-relation-impact-sweep’s Finding 1, and the finding’s own premise was overtaken before the delivery, so a later sweep should not re-derive the exclusion: the participant fan-out landed on trunk the day the item reached Ready and is not an ancestor of the sign-off, giving intent_field_scope_table its PARTICIPANT_TABLE arm, so the coordinate the audit and the signed-off spec both recorded as producing no instruction row now produces one row per table-bound participant, on both bare inference bases, at the argument site and the input-field site alike. What ships is therefore a limit rather than an exclusion. The rows arrive without the branch: the relation is keyed on the use site and the one table the site’s content binds against, so a use site with two table-bound participants is two rows differing in node_type_name and in nothing else, recovering the pairing through the node type’s own table is unsound in general, and the relation’s comment now says so, states the two shapes the fan-out does not reach (a written typeName:, a discriminated interface binding one table), warns against relaxing the single-candidate demand, and records the participant-keyed spelling (graph_name, site, type_name, field_name, argument_name or path, participant type name) exactly once, the arm itself declined on the two-reader rule until a second reader asks. Six store pins in NodeIdInstructionTest cover the fan-out at both bare sites and both container spellings with the explicit-typeName: sibling and the single-table discriminated interface as controls, one pinning the limit directly by asserting the two rows agree on every column but the node type, plus the decode-defect view staying empty there; the pipeline tier gains the mixed shape beside the R673 cases, one participant node-backed and its sibling not, as a typed rejection asserting no message prose; and resolveNodeIdArgTargets’s javadoc states that the producer owns the use-site-times-participant grain the population reaches without keying. Independent-session In Review → Done review, the reviewer sharing no session with any implementation commit: full reactor green under `mvn install -Plocal-db against a private repo head and a per-worktree database (BUILD SUCCESS, NodeIdInstructionTest 25 of 25, MultiTableFilterLoweringTest 16 of 16). The polarity flip shipped as an author-side amendment rather than a Ready → Spec reopen, and the amendment handed that instrument question to this gate; it is ratified here rather than bounced, because the Ready-approved decision content, no participant-keyed arm, the key spelling, pins at both tiers, the scaffolding sentence, survives the flip untouched, and the flipped fact was verified against the tree rather than read off the amendment (the fan-out arm and its relation exist as claimed, the store pins measure the two-rows-per-use-site behaviour directly). User-facing-doc check skipped, the item touching no docs/ file; retirement sweep skipped, nothing retired; no code-string assertions on generated method bodies, the new pins being store rows and classification verdicts.

  • R676 (e79578484 the decode routes and the escape, a748e1490 tests, fixture, docs and the tightened refusals, 92c565c19 the argument coordinate’s lift test; filed 822673552, Backlog → Spec eb3e6ffae, Spec → Ready 0434a855a, Ready → In Progress 48bbbddf2, In Progress → In Review 5d06eaaae, rework round 420e4aa3c/fcde2393b, back to In Review ce7fb5331; Done-gate review in this commit): a @nodeId filter input on a multitable query can state a per-participant join path, and @condition(override: true) genuinely takes the predicate where no route resolves. @referenceFor widened to INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION under one direction invariant read at both coordinates (the path runs the way the generated join runs, starting from the table the query is standing on); route selection in NodeIdLeafResolver.resolveFkJoinPath became a sealed choice (uniform @reference, per-participant @referenceFor, auto-discovery), Resolved gained the AuthorOwnedPredicate arm, and the three auto-discovery refusals name @referenceFor and the override escape under a participant while single-table wording stays unchanged. lowerParticipantFilters stops short-circuiting on the first failing participant and rejects a leaf whose participants split between the generator-routed and method-owned contracts, the one site that can see the split at all. The whole-schema unmatched-type: check ships as a derive/ join (ReferenceForParticipantDefects) rather than a stored view, the argument coordinate gains the graphitron_argument_reference_for capture family, and the docs land in referenceFor.adoc, the global-id how-to, nodeId.adoc and condition.adoc. Four deviations disclosed in the spec body rather than silent: the participant rides NodeIdArgPlan beside ClassifyContext, the detection is a derive-side join, two refusals stay refusals under override (a malformed leaf and a stated-but-wrong route; only an undiscovered route escapes, which keeps the change monotone), and the execution-tier override fixture cannot reference NodeIdEncoder because @condition classes compile upstream of the emitted sources, so it filters on a shared column and the encoder pattern is documented instead. Independent-session In Review → Done review, the reviewer sharing no session with any implementation commit: full reactor green under mvn install -Plocal-db against a private repo head and a per-worktree database (BUILD SUCCESS, zero test failures, the documented flaky execution test quiet this run). Round 1 of the gate bounced the item for one completeness gap, the argument coordinate’s author-owned lift being exercised by nothing; the closing test asserts the classified carriers per participant and was confirmed the sole reacher of the minting arm by making it throw. Completeness was checked against the spec’s own Tests section bullet by bullet: per-participant routes at both coordinates, the override-merge, the inert-reuse case, the four one-leaf rejections, the override ladder with its boundary pair, the split-contract rejection, the three-participant aggregation, the whole-schema typo family, and the execution fixture where film reaches language only ambiguously and inventory only through film, so the union is authorable only per participant, with the wrong-typeId client error and the escape running the author’s method against each branch’s own table. NodeIdOverrideConditionFkTargetPipelineTest and the single-table GraphitronSchemaBuilderTest rejection rows verified untouched; no code-string assertions on generated method bodies, the pins being classified carriers, minted diagnostics and executed behaviour; user-facing docs carry no transient citations; retirement sweep skipped, nothing retired.