Rewrite Changelog
-
R840 (
325d3284e`+87b06ebc0` slice 1,317b08b93slice 2,341a758a3slice 3; In Progress → In Review323454bb3, Done gate in this commit): The spec-by-example corpus became a folder of self-describing, fact-first SDL documents. All 57 fixtures live asgraphitron/src/test/resources/corpus/<id>.graphqlswith the prelude a document beside them;ClassifiedCorpusand the Java prelude string are retired, andCorpusDocumentsloads 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 throughStoreCatalog, well-formedness, an assertable-population fence and CHECK-membership checks, planted regressions under each.code-generation-triggers.adoccollapsed from 2474 to 1208 lines: 32 generated-and-committed_example-<id>.adocfragments are simultaneously the page’s includes and the emitted-names approval files, held byCorpusFragmentTestwith three placement floors (a fragment with no document, a document with a projection and no fragment, a fragment no page includes), retiringClassifiedDocTest,OutcomeBlockDocTestand the paste loop. Adding a worked example is now a document, a regenerated fragment and one include line, no.javaedit. The per-axis@classifiedretirement waits on R682’s relations; each surviving tuple axis is a visible request for one. -
R814 (
b868b9947-e3ddcd419the slice-3 promotion round,9e567f89fthe rework; In Progress → In Review6852fa364, In Review → Ready151cb1799, Done gate in this commit):docs/architecture/reference/code-generation-triggers.adocdescribed 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_claimand 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:ArchitectureDocSymbolGuardTestresolves every backticked type span on an architecture page against the reactor classpath, andTransientCitationCheckgrew a walked habitat overdocs/architectureanddocs/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 aroadmap/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 underroadmap/, 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 undermvn install -Plocal-db. Completeness was checked against the item’s own four goals rather than the build, each verified against the tree: zeroR<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.nameunder@field(name: "country")rendersTABLE_COLUMNin 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:DmlBulkMutationsExecutionTestseedsfilm_actorrows thesplitTableField_bridgingConditionJoinassertion 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.theBlockRendersNoCommandRowsAndNoBodiespinning names and parameter lists as the ceiling. -
R832 (
47cae33a8the budget split,72a09d416the re-measured ceiling; filed32d1299ac, Backlog → Spec61a879a6d, Spec → Ready24c63d94c, Ready → In Progress47cae33a8, In Progress → In Review053d577cb, In Review → Ready6e4e0c3d4, Ready → In Progress6d0e6b34d, In Progress → In Review118fa378f; Done-gate review in this commit):CatalogRefreshTest’s single `WAIT_MScarried 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_MSis a bound on failure, paid only on a red run becauseCountDownLatch.awaitreturns the moment the latch counts down, so both positive awaits take it and a generous figure costs a green run nothing;QUIESCENCE_MSis aThread.sleepthe 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 coldSourceWalkerparse through the Compiler Tree API plus the jOOQ writes that land it, where theSchemaWatcherTestthe figure was inherited from triggers no-opcountDown()runnables; that test keeps its ownWAIT_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 undermvn install -Plocal-db(BUILD SUCCESS, 14 modules, none skipped,CatalogRefreshTest3 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, aSystem.nanoTimebracket aroundrefreshJavaSourceswith 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 thattest-javadoc-no-forkis 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 nodocs/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 (
8c03600the four instruments and the page rule,1472d69the recipe’s outer loop,3665bd1the profiler’s self-sampling clause; driven end to end and rolled back across5daaeaa-1ae9a48, Backlog → Spec3e47227, spec review6be3615, spec revision6f5db57, Spec → Readya815b86, In Progress → In Reviewf216109, Done-gate review7ea2d25, rework returned5d34775; Done gate in this commit): thestore-performanceskill no longer hand-rolls a timing the database was already keeping.SET QUERY_STATISTICS TRUEandINFORMATION_SCHEMA.QUERY_STATISTICSreplace theSystem.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:ConvertTraceFileat trace level 2 besidereport-inline-multiplicity, as the measured counterpart to that tool’s static breadth ranking; trace level 3 behind the existingSTORE_EXPLAINguard, for the planner’s own per-index cost evaluation, whichEXPLAIN ANALYZEnever reports since it names the plan chosen and not the plans rejected; andorg.h2.util.Profilerinside step 1, which keeps its place in the evidence order and stops being a dead end, itsdepthbeing 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_TIMEreads 0.0 always and is neither a floor nor a tell, a row atEXECUTION_COUNT1 ranks and does nothing else, and the statistics are the store’s rather thanStoreConsole’s, that being a second database of linked tables. `docs/architecture/explanation/fact-model.adoctook 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 untilOPTIMIZE_REUSE_RESULTSis 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_TIMEhaving 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 onGraphitronModelStorebecause 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 (
ea73eed9classification and emission,7e107e36the execution fixtures, the SQL baseline and the manual page,9e5280d6the lateral throw generalised off its original caller; filed and specced across9f6924ab-eb976b34, Spec → Ready218994c3, Ready → In Progress46e0712d, plan departures recorded5b74f76a, In Progress → In Review21d07484; Done-gate review in this commit): a{condition:}hop is now legal in a@referencefilter 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 correlatedEXISTSa 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.terminalTableForReferencebecomes total (TableRef, notOptional), because a hop’s target resolves off itsTableExprand never off itson(), 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.resolveConditionJoinTargetcollapses its terminal-versus-intermediate fork into one rule keyed on the available source,declaredTargetor 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.FkHopretires in favour of aReachPath(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.hopZeroCorrelationextracted fromcorrelationWhere’s `OnParentJoinarm andemitBackwardBridgingfor interior hops, leavingConditionGlueRendererwith noOnswitch of its own andJoinFragmentsbelow the narrowing where its charter puts it. Two things ride along. The output-side scalar@referenceleaf, 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:reachExistsnever calledappendHopFilters, so a{key:, condition:}filter path emitted anEXISTSwithout 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@conditionon an FK-target@nodeIdfield, restated fromstructuraltoRejection.deferredbecause 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.adocretires the "Foreign-key hops only" rule and three stale claims (thecondition:form is not a runtime-throwing stub,@reference(path: [])does not fail parsing, and a hop’s own predicate is emitted inside the filter’sEXISTS). Coverage across five tiers, the newConditionGlueRendererTestamong them. Independent-session In Review → Done review: fullmvn clean install -Plocal-dbgreen; 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 (
2a446ec9dthe delivery,b3c437fe2the rework round; spec body1c30a4a78, spec review5a5b59ac2, revision86639003a, Spec → Readye9d1349ea, In Review4b8ea180f, In Review → Readyf13298dab, back In Reviewb4baed7fc; 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.resolveNodeIdArgTargetsis the one producer of the cross-participant verdict, sealed asSharedTarget(every participant decodes one node type,ThrowOnMismatchunchanged) orPerParticipant(aSequencedMapof per-participant decode refs, the map itself the dispatch fact, reached through oneParticipantFilterField.nodeIdArgDispatches()accessor;SequencedMaprather than the plan’sMapbecause 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 secondNodeIdDecodeKeysarm,PruneOnMismatch, the mode ternary in the glue becoming an exhaustive switch on the seal, andConditionGlueRenderer.appendPruningAndrenders the mismatch-vs-absent trichotomy per cell so a present-but-mismatched id prunes toDSL.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@asConnectionone, 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-@nodeIdleaf 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 acontains("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 undermvn install -Plocal-dbagainst 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@nodeIdreference chapters, retirement sweep skipped with no vocabulary retired. -
R818 (
b09afc1the surface, the SDL type arm and both dispatch-matrix guards,bc84555the directive-target arms; filed and Backlog → Speccae3a1c, spec review round 11215dd1, revisiond264f34, Spec → Readyca3f174, Ready → In Progress folded intob09afc1, plan recordedcb70750, In Progress → In Review6235f2e; Done-gate review in this commit): the language server answerstextDocument/references, so an author about to rename a table binding or retire a@serviceclass can ask what in the schema uses it instead of grepping for a word a description might also contain.referencesProvideris registered andGraphitronTextDocumentService.referencesanswers onStoreRead.REFERENCESthrough the interactive door, the definition handler’s shape throughout: one span, one store read, oneStoreAnswerswitch, an empty list when the budget expires. Two arms behind it, keyed off disjoint syntax the way the definition chain already is.TypeReferencestakes a cursor on a type declaration name or on a use of one and reads the four use populations (graphql_fieldandgraphql_argumentbynamed_type,graphql_implements,graphql_union_member) as one statement, honouringReferenceContext.isIncludeDeclarationand listing every declaration site rather than the one a jump would pick.BindingReferencestakes a cursor inside a directive argument and dispatches on the exhaustiveBehaviorswitch, 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 thatIntraSchemaDefinitionsprefers 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:Reachnow requires every surface to carry a verdict on every trigger, with no complement-filling convenience, so a constant added toLspSurfacefails the build until all twenty-one rows have decided about it; andLspSurfacestates 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 onfilmrather 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 atIn Review -> Doneagainst 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 (
bb0e9d47the alias namespace and the type-scoped fold,0cf1b65bthe census plus the fan_target fixture and the doc promise,64a38143the round-2 argument pin on a fixture that can express it,981daf28the codegen schema-version bump; filed743cb893, Backlog → Spec91e3ad12, spec revisions8f1a379d+57caa525+12ad1b9f+2728e5d9+4e49fc9e, Spec → Readya2f9629e, Ready → In Progressc2a5d6ef, In Progress → In Reviewe233bb1a, In Review → Ready153596e3, Ready → In Progressb47323741, round 2 recorded6e6868c2, In Progress → In Review6478ffe5): 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’sLinkedHashSetdropped 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 inFieldBuilder.aliasOwnerOfoff the indexTypeBuilder’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 incode-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 atIn Review -> Donewith the emitted tree read rather than the argument: the fold emitsSet.of("marks", "target"), each participant’smarksarm correlates on its own FK under__rk_Fan<Alpha|Beta>$whiledetailskeeps 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.hopPairsjoinedintent_foreign_key_column_pairon aCASEoverfk_on_from, and the mechanism the commit named is real:intent_field_chain_nodeis recursive and carries four window functions, so no outer predicate prunes it, andfact-model.adocprices 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, whichstore-performancenames 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.produceover 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. Thehopsreading beside it keeps its ownCASEand always could: it joinssql_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 1Cthat 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 inmeta_materializeor explicitly exempted, was already out of scope in its own body and wants an item of its own;intent_mutation_routine_seatis 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 (
045ef6ca9the delivery,0d1e6a64f+490674b7dthe corrections; speccedd972352-reviewed and5435635-revised, Spec → Ready0745ac4dc, Ready → In Progress88d7323da, In Review3c3ac9384; 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 aWITH RECURSIVEreader 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 aCOMMENT 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_pairandintent_node_id_decode_hop_columnboth earning one andintent_resolved_type_bindingandintent_field_column_scopegoing to the roster instead, the first of those declining the single largest total gain available because no shape of it spares every reader. Nothing isUNIQUE, 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.analysesupplies 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 ofANALYZE, verified by inserting, analysing and rolling back to find the row still there, sorefreshAllanalyses inline andFactCaptureanalyses after its transaction closes. Both refill paths are covered.MaterializeRegistryGateTestgrows three gates, index-or-rostered-with-a-reason asserted by equality onHAND_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. `SurfaceScanCountTestmoved 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.adocstates the conditional and the corrected mechanism. Independent-session In Review → Done review: fullmvnd install -Plocal-dbgreen; the one red run before it wasGraphQLQueryTest.splitTableField_conditionJoin_returnsActorsPerFilmobserving afilm_actorrow a concurrent mutation test transiently seeds, diagnosed toDmlBulkMutationsExecutionTest’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 onlydocs/change being the contributor-facingfact-model.adoc. -
R819 (
1f8505076+5e78faeb2the delivery,fd1c4120athe schema-gate fixture; filed5f0b9655c, spece40993854, Spec → Ready3e545b2a0, In Review85c8d694d; 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_fieldandintent_carrier_data_fieldmove onto the capture cadence (rules kept in_liveviews, tables under the canonical names, registry reasons stating the measurements), and the carrier body’s windoweddata_channelCTE 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). Theintent_poly_memberslice 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 (
42614bdthe delivery; filed at Specaabf6c8, spec reviews86ae74d+5e38d68+9c51600, spec revisions12af0dc+3516a32+587c9f7+099054b, Spec → Readyf3ec0af; 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_familygains anintroductioncolumn, 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_headlineauthors 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_bridgedeclares 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_referencederives the declared key edges out ofINFORMATION_SCHEMAat constraint grain, both ends carrying their census family.StoreCatalogreads the three new relations verbatim,GrainSentencelifts a relation’s first sentence beside it because that convention is the store’s and not the renderer’s, andStoreProsebecomes 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:FamilyRosterGateTestcloses 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),GrainSentenceTestpins 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,SchemaReferencePagesTestpins each renderer floor with its own negative case, andSchemaIdentifierDriftCheckgains 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 fullmvn install -Plocal-dband by reading the rendered pages: all thirteen family pages carry the three new sections, and CI’sdocs-deploypublished them. -
R793 (
272ef13the delivery,fc4535bthe rework round; filed and specced with the roadmap sync0e643f3, Spec → Ready72f16f7, Ready → In Progressed71195, In Progress → In Reviewf163ba7, In Review → Readyd033130with a corroborating second gate ate9c39d0): 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_seedmeasuring 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 forDeclarationFactswas unnecessary here because the ruinous term sits one relation further down.intent_resolved_type_bindingcarries aCOUNT(*) 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 onsql_constraint_column, re-entered per row by the unresolved-path arm’s correlatedNOT EXISTSagainstintent_field_column_scope; registering the scope too took one evaluation ofintent_field_column_tablefrom 131 s to 144 ms. Whole statement: 191 ms, against a 3 s interactive budget. No Java changed. Bothmeta_materializerows carry the arithmetic in theirreasoncolumn, 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 isDiagnosticsStatementCountTest.theDrainsStatementStaysCollapsed, which captures the drain’s own statement andEXPLAIN ANALYZE`s it for a total scan-count ceiling, the clock-free currency `SurfaceScanCountTesthad 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 undermvnd install -Plocal-db; no code-string assertions on generated method bodies; user-facing-doc check and retirement sweep both skipped, the item touching nodocs/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 (
ed424f6the delivery; filed at Specaa9dad2, spec revisionsbe57ab3+0f6a090+fb466c6, Spec → Ready1ae37d3, Ready → In Progress4948095, In Progress → In Review24c0c4c): 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.coordinateand the_upperfolds pass, andintent_type_backing_conflictcarries the whole lesson in one relation,candidatespassing whereclass_namesfailed 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,DiagnosticFacetsfilteringdirectiveswithisNotDistinctFromagainst the joined set, so an author asking for conflicts involving@servicesilently got only those whose entire set was exactlyservice. The MCP surface trades that dimension for adirectivefilter that asks membership by joining, andFiltersits besideDimensionrather 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 isAuthoredClaim’s declaration order, which is not a captured fact of any graph, so no view can state it. `intent_authored_claim_rejectionholds it, written at capture cadence, withkindandvariantminted alongside fromRejectionKind.ofandRejectionFacts.classSpelling, which took bothRejection.*string literals out of the DDL and dissolved theRejection.*half of R807. The load-bearing test is not one the spec asked for:theMintedRejectionMatchesTheViewRowForRowAndWordForWordpins the one-to-one the diagnostics inner join rests on, without which a missed mint would drop a diagnostic silently.CollectionValuedColumnGateTestdenies the five constructs in the DDL’s statement regions, blanking--comments and literal bodies in one pass so the file’s own prose aboutLISTAGGcannot fail a clean tree, and discloses in its javadoc the half it cannot cover:diagnostic.directorywas 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 (
10b7e96the delivery; filed inside480bb20, renumbered61ffdf7, Backlog → Specff5b49a, Spec → Readyf8963a0, Ready → In Progress70c8727, In Progress → In Review3bd53a4): 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 timesINTERACTIVE_READ_BUDGETand 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 from272ef13^andgraphitron-modelrebuilt, that window returns to 10205 ms and 561851 scans, so R793’s twometa_materializeregistrations, made for the diagnostics drain and not for this surface, are the whole difference, andintent_resolved_type_binding’s `COUNT(*) OVERis 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@fieldsites 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 thatSurfaceScanCountTest, 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. SoINLAY_CEILINGis 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; andtheInlayReadCostsABoundedAmountPerDeclarationstates 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.scaledSdlgained a sites dimension while its one-argument form stayed byte-identical, so the census test’s fixture is untouched, andscansForandrecordingnow 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, withtheCensusLookupDoesNotTrackTheSchemasSizestill green, and all four per-declaration figures were read back individually by lowering each ceiling to 1. Full reactor green undermvnd install -Plocal-db. User-facing-doc check skips, the item touching nodocs/file; the item retired no vocabulary. -
R794 (
60397efthe delivery; filedf8ddef1, Backlog → Spec78f94d7, spec reviewsd812813+278334b, spec revisions077c173+04f89aa, Spec → Readye8d8fa2, Ready → In Progressf13e6b7): detaching an editor fromgraphitron:devno longer fills the dev console with stack traces for answers written onto the socket the teardown just closed.LauncherFactoryingraphitron-lspbuilds the lsp4j launcher both transports use, the stdioLaunchermain andDevServer.serve, so the connection policy is configured once and a third transport inherits it; the policy isquietOnStreamClosed, applied throughLauncher.Builder.wrapMessagesso it covers both message directions, consultingJsonRpcException.indicatesStreamClosedon aconsumefailure, dropping the message at slf4j debug on a true verdict and rethrowing everything else so a framing or serialisation failure stays loud.wrapMessagesrather thansetExceptionHandlerbecause a handler is never consulted for a notification, which is what apublishDiagnosticspush 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 aSEVERE-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 `AtomicReferencewithclearRecalculateListenerassetRecalculateListener’s compare-and-clear inverse, the service holds its listener `Runnablein a field created once and surrenders that same instance from a newdisconnect()chain (GraphitronTextDocumentServicetoGraphitronLanguageServertoDevServer.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. TheRejectedExecutionExceptionabsorption 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.notifycatching 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: withwrapMessagesremoved both end-to-end pins fail on exactly the two records the item removes,SEVERE: Internal error: ... SocketException: Socket closedandINFO: 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 failsConnectionTeardownTest’s positive case. Full reactor green under `mvnd install -Plocal-db,LauncherFactoryTestandConnectionTeardownTestre-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 (
8284400the named-parameter carrier and the bean-member deferral,c79f4fdthe bare spelling,8df0217the scope’s second rung; the relations and their stages at4548c98f,705f96b6,e1ad3ae3..375943a9,e578ef47..e9dc149f,417298d3,cb502a21,9c801d16,12dabb11,2e46090a,d0f6358c,6d70b6a4,8b819323,270cc6a3,2d7223b4,7287451e,5335f0ec; In Review → Ready03267bc,80c4007,3dc5ca4):@nodeIdencode 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_defectmints 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 byNodeIdDecodeDefectsas a fourth component onStoreDetections. The junction chain stops being a rejection and becomes absent local columns reaching the hop-generalEXISTS, pinned by execution-tier row counts against PostgreSQL rather than by an argument that theEXISTSmultiplies nothing. The read family gains its encode at the accessor, by-name, typed-column and@errorarms. 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 (
b4a8eccthe delivery; filed and Backlog → Spec1ae8063): a friendly explanation article on the fact store’s naming discipline.docs/architecture/explanation/naming-the-row.adocteaches "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 sanctioneddiagnosticexemption and the disclosed inversions. Linked from the explanation index and fromfact-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 (
62343d2the census cut, the classified list and the enforcer,2d8ad9athe manual sweep at the Done gate; filed1e3c082, Spec → Ready withheldf6ce1a9, author revision8e3cead, Spec → Readyda011cf, Ready → In Progress4b78ba3, In Progress → In Review748743b, In Review → Ready87f44bc, Ready → In Progress1ba1bea, In Progress → In Review09a2458): 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 forgraphitron-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.classbytes), 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 byAbstractRewriteMojo.resolveCompileClasspath(the earlierresolveClasspathRootshalf 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 skipsTRANSITIVEbefore opening it, so census subset-of loader is a derivation over one list instead of a promise about two thatbuildCodegenLoader’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:ClasspathNameabilityis 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_classfilters 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 onSIBLING, the one arm whose existence the design argues for. Nor is itgetProtectionDomain().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 declaredgraphql-javaclass 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.SIBLINGis 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 everyClass.forName:@service,@conditionand@externalFieldinServiceCatalog,@scalarTypethrough a newScalarResolution.Rejected.UndeclaredClassarm,@errorinTypeBuilder, and@sourceRowinSourceRowDirectiveResolver, the last two being sites the plan’s own enumeration had missed. Because an enumeration defended only by review is the thing that rots,CodegenClassForNameGuardTestfails the build on anyClass.forNameingraphitronmain sources carrying neither anameability: checkednor anameability: 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 antipatternmaven-dependency-plugin:analyzeexists 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 twoDiagnosticsarms 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), andClasspathSources.recordstates the new silence as design, a skipped entry producing nostore_sourcerow 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 ofjvm_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 inexternal-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@conditionand@externalField, and three the per-directive reference entries for directivesexternal-code.adocitself 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 atexternal-code.adoc#make-the-class-nameablerather than restating it, and withdrewmojo-configuration.adoc’s surviving "rare legitimate case" for `<plugin><dependencies>version pinning;custom-scalars.adocwas already correct and stayed untouched. Second cycle approved on both gate questions: full reactor green undermvnd install -Plocal-dbon the exact reviewed tree, the newtransitive-not-nameableinvoker IT failing a real Maven build namingorg.jooq:jooqand asserting nothing was generated,check-adoc-xrefsresolving 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 adoesNotContainguard inDiagnosticsTest), and the user-facing-doc check clean over six touched.adocfiles. Two non-blocking notes recorded at the gate:classifyElementadds a fifth arm the plan does not enumerate, an unattributable path classifiedDECLAREDwith no coordinate, which fails open, is argued in the javadoc and is pinned byClasspathClassificationDecodeTest.anUnattributablePathStaysInTheCensus; andRecordBindingResolver’s author-written `classNamesites are marked exempt rather than checked, becauseServiceCataloggates the same@service/@externalFieldnames 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: anorigincolumn on the store-global, definition-keyedstore_sourcewould be silently wrong the first time two modules in one reactor disagree. -
R787 (
5dc2452the ordering change,7c3a305the coverage judgment,b113aacthe symmetry correction; filede989a82, Backlog → Spec with consult findings8d94440, Spec → Ready31bf3a4, Ready → In Progress6697f47): 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 readssync -> 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 ofCLAUDE.md"Building and testing", absorbing the-plpreference 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.mdbecause a mandatory rebase makes the documentedinit.sqljOOQ-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.mdstates 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 (amvn -pl roadmap-tool exec:java -qregenerate 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.adocand theroadmap,srpandclassified-corpusskills 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,roadmapandsrpalready fetch and rebase before acting, andclassified-corpusis the one documented verify-then-publish sequence but verifies with scoped-pl :graphitron -am testrather 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 undermvn install -Plocal-dbon 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-enumerationall reporting clean, the citation guard being the one that covers this item’sCLAUDE.mdedit); 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 nodocs/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 (
7f78771eboth deliverables; filed4e0c419e, Backlog → Spece9104229, spec reviewscceea3ef+c7aa2182+663d4c0e+0339b737, Spec → Ready5562ba7e): store slowness in a derived relation is now a database question with a documented first hour.docs/architecture/explanation/fact-model.adocgained 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.mdcarries the ungated half, posture and order of operations, on the split the item settled: the page is scanned bySchemaIdentifierDriftCheckand 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 (
e12adb06the fix; filed8626e5a6, Backlog → Spec733e9515, spec revisionf57a27f2, Spec → Ready8a65e82c, Ready → In Progress06f167eb, In Progress → In Reviewbb753b9e): twoTenantScatterSubstrateTesttests waited for a recordedconnect:<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 recordsconnect:from insideprepareStatement, which runs insideentryFor’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’sevents.clear()wiped the abort, andreleaseAllfound nothing left to abort, so the assertion failed on a missingabort: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 afterentryForhas returned; from there the self-abort path is unreachable andreleaseAllis provably the only remaining aborter. That is a proof from the control flow ofperTenant.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 missingabort:Awithabort:Bsurviving, post-fix 3/3 green with the probe still active, and 9/9 green without it. Thestraggler_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 insidescatterthat no external latch can gate. Independent-session In Review → Done review; full reactor green under-Plocal-db. -
R750 (
3fa5432the mechanism, both guards and the model retype,e4f5498the fixture conversion; filed743cb89, Backlog → Spec1411e45with the body drafted atbe48260, spec reviews502f286+191c863, Spec → Readycf593c9, Ready → In Progress2121216, In Progress → In Review55b27da): a@discriminateinterface over a Postgres-enum discriminator column now generates SQL the database accepts. It previously failed on every query withoperator 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 asDSL.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.DiscriminatedTableFragmentsmints the qualified reference and the typed operand once each, all four sites calling the reference mint andPathFragments’ 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 generatedTypeResolverreads back asString, not a comparison operand. Two guards make the mechanism total. An unresolvable@discriminate(on:)is a classify-timeAuthorError.UnknownNamenaming the table’s columns as candidates, replacing a silent raw-string fallback that only ever emitted code failing at query time, which makesTypeBuilderthe 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.valconverts an unknown literal tonullwith 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_NAMEfor the GraphQL-enum caller whose generated coercion callsvalueOf,DATABASE_LITERALfor the directive-value caller, sincempaa_rating’s `'PG-13'literal is the constantPG_13; the literal read is reflective rather than a cast toorg.jooq.EnumType, the class coming from the codegen loader.TableInterfaceType.discriminatorColumnand every carrier below it hold the resolvedColumnRefinstead 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_typeandjti_subject.subject_kindbecome Postgres enums (with the schema version bumped so an incremental build cannot reuse the stale catalog), reaching all four emission sites, whileparty_kindstays varchar as the control carrying both rendered forms.content_kindgains 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 undermvnd 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 DMLvalues (?, cast(?...))) with the__discriminator__projection uncast;party_kind’s six statements are byte-identical at `= ?andin (?, ?); both guards reach a build failure throughvalidateUnclassifiedType. 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.adocfiles, including apolymorphic-types.adochow-to sentence the spec did not enumerate. Retirement sweep skipped, noRetired vocabularydeclared. One acceptance criterion is met by substitution and disclosed as such: the reproduction shape classifies with its enumColumnRefinDiscriminatorColumnGuardPipelineTestbut 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: aString-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 withString.valueOf, and a jOOQ-generated enum carries its literal ongetLiteral()without overridingtoString(). That is invisible in the tree only because every seeded literal in both converted families is a valid Java identifier. -
R755 (
1c4028d; filedfe7d0d0, Spec revisionsd4b4cb6+f40e290+7480379, Spec → Ready328f8de, In Progress → In Review97f1bca, spec collapse4b6bc75): Thesrphand-off templates led with commit bookkeeping and gave the gate questions one sentence apiece, so reviewers came back with nits;roadmap/workflow.adocnow 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 (
4683e29the mechanism with zero rows registered,1f87258the two registrations,b4316b1the multiplicity report and the shared canonical tree and clean removal,1c79305the two follow-on re-measurements,3506105the Done-gate rework; filed and specced across14b0126+c5f8978+48ce5ce+773bcd1, Spec → Ready11888bb, spec corrections6bfcadd, In Progress → In Review26ca19a, readability pass42edd94, In Review → Readyeb406d1):GeneratorDeterminismTestcost 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 ofintent_argmapping_projection_defectexpanded to 2066 relation instantiations, namingintent_argmapping_pair55 times andintent_spelled_table39 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:EXPLAINcosts 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_materializepairs 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_livesuffix, the sixteen other view bodies namingintent_argmapping_pairand the six namingintent_spelled_tableare not edited at all, they simply stop hitting a view and start hitting a table, and so does each relation’s one Java reader.Materializationsingraphitron-modelrefills every registered target inside the capture transaction, per graph where the target carriesgraph_nameand whole where it does not, and that module placement is forced rather than preferred: the seeded fixture lives in its test sources and cannot reachgraphitron, 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’sNOT NULLreasoncolumn 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, andFixtureWarningsGateTest56.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, whichGeneratorDeterminismTest’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-modeltest 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.SeededStoremodelled 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 themeta_family is now exempt for a reason about authorship rather than cadence; and the DDL file’s firstINSERTneeded 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 ofintent_resolved_node_key_projectionfell 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 aroadmap-toolstep in the family ofAdocXrefAnchorCheck, 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.everyMaterializedTargetEqualsItsRuleasserted nothing: that class’s fixture declares noargMapping-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 unscopedDELETEinrefreshPartitionleaving 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 undermvn install -Plocal-db, 14/14 modules, no code-string assertions on generated method bodies, nodocs/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, `DevMojobeing the only store-open outsideFactCaptureand 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_livecolumn comments the comment gate forces,readAllsurviving only for anisNotEmpty()check,graphKeyedre-readingINFORMATION_SCHEMAper 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 underdocs/mentions the registry andfact-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-oncleared here. -
R668 (
4195667the detection relations,8c60ad3routine writes emit from a command row,1a91a24the projection,a807f3bthe openability rule in the grammar,a2607d8the@conditionarm and the type gate,04a8cc8the walk stops judging,85a04bbthe per-schemaTablescapture,2ef0b57the producer reads the store,25814a1the Done-gate rework; filedd733f40, Backlog → Spec813786e, fourteen spec revisions301d084+73551be+cfd84eb+1b4b8e6+b267458+169b845+93b7942+3f3cea5+46b3ede+9df5780+ecf2324+9ab6700+3a8111b+4f8cb41, attribution fixf79a112, In Progress → In Review52d6e2d, In Review → Ready444ac66, Ready → In Progress84fda8a, In Review2ccecdd; the Spec → Ready and first Ready → In Progress flips fall inside a squashed clone baseline and have no individually citable SHA): anargMappingpath may open a@nodeIdleaf into its node type’s key columns, which closes a silence rather than adding a convenience. Before this,pCustomerId: input.customerIdon a@nodeId-carryingIDcompiled clean and sent the base64 wire id to anINTEGERroutine 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 anIDcarrying@nodeId(typeName:)opens into that type’s key columns, soinput.customerId.customer_idis a path andcustomer_idis 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: sevengraphitron_*_arg_mapping_pairrelations reduce to oneintent_argmapping_pairwith 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 isintent_argmapping_bound_parameter_type, because the two populations answering "what Java type does this pair’s left side take" are unrelated: a@routineparameter’s type is a catalog fact and every other site’s is a classpath fact, so a reader had to switch onsiteto 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 primitiveint, 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_defectreads the same chain and states six verdicts with no precedence rule, its arms disjoint by construction overnode_id_declared,trailing_segmentsand one existence test; the reviewer re-derived the disjointness pairwise rather than taking it, the load-bearing step being thatnode_type_ref IS NOT NULLimpliesnode_id_declaredin the leaf view, which is what keeps the type-mismatch arm from colliding withMISSING_TYPE_NAMEorUNDECLARED_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_SITESnames the three wired sites and@service, the input-field@conditionand the three reference-step sites each fail the build saying so. R735 carries the list-shaped emission and theArgBindingMap.Resultdeferral arm. One deletion is the shape’s foundation:ArgBindingMap.ofruns 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_IDandTRAILING_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_fqnis the per-schema generatedTablesclass, needed because a store-sourcedTableRefcannot otherwise name a column constant, and it follows the rule itskeys_class_fqnsibling 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 butperSchemaTablesClassesAreDistinct, 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.decodeBindingTypeis the third way to get acolumnTyperight, beside a sibling record’s decoded one and a liveClassat the reflection boundary, and the one a store-sourced reader needs; it is array-safe because the captured name is the rawClass.getName()form, whose JVM descriptor is exactly what crashesClassName.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.producetakes noGraphitronSchema,command/KeyProjectioncarries 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’sfromArrayload 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:ArgmappingKeyProjectionEmissionPipelineTestpinned 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 throughTypeSpecAssertions, 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 itsorg.jooq.Conditionreturn 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_badNodeIdIsARequestErrorAndCommitsNothingsends 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 undermvn install -Plocal-db, the emission test carrying no rendered-body assertion (its one survivingcontainsreads a method-name list), user-facing-doc check clean over thenodeId.adocandroutine.adocadditions, 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 tradeoffTypeSpecAssertionsexists to make and not an elimination of the scan; andinvocationTakesProjectedReadadmits 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 fromintent_resolved_node_key_columntoday and needs no new capture,graphitron_argument_node_idandgraphitron_field_node_idalready carryingnode_type_refat both coordinates. What is missing there is only the consumer,ArgMappingCompletions.rightCandidatesreturning 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 absorbedargmapping-completion-after-a-dotitem’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 thegraphql_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 neitherDeliveryResiduenorDeliveryShadowTestis 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 isintent_resolved_type_bindingrather thanintent_bound_table, record-handedness is the backing closure rather than the producer-payload seed set, authored type and cardinality read throughgraphitron_field_synthesis, the structurally declared connection’s element resolves through no shipped relation), and the sibling relationship tointent_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 (Nodesgrown into the toolkit plusGraphqlNodeKindasDeclarationKind’s structural sibling, retiring 12 `childOfKindcopies and three latent NPEs),DirectivePolicycentralizing the directive-name carve-outs withDefinitionsjoining the coordinate-driven dispatch shape, and theCompletionProvider/Completionssealed-switch dispatcher replacing the hand-ordered waterfall, with theInferredDirectiveRendererregistry andInlayHintRendererCoverageTestclosing 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 aroundfacts/store readers and theLspSurface/TriggerDispatchmatrix,Workspace’s four torn-readable volatiles became the single `StoreAccesshandle the item’s own 2026-08-06 fact-base note predicted, theLspVocabulary.load()overloads lost their last caller,WorkspaceFilegained close discipline, and the native-library probing duplication left with thegraphitron-tree-sitter-nativesbundle. A 2026-08-19 audit of the remainder against the tree found three small surviving facts (outbound byte-range toRangeconstruction still per-feature,CodeActionsrunning 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 (
28c4f64the qualifier split, the folds and the eleven write conversions,71b7473the narrowed fold rule,c1dc70cthe fact-model page,0d4e2d1the Done-gate rework; filed7594dcd, Backlog → Specd4f2666, six spec revisions034a212+ff2f8a3+ed6fa19+8cb74b9+3eb2b78+599ce61+3474cb2, Spec → Ready7d4404b, Ready → In Progressdf845d2, In Review4e7c5b9, In Review → Ready94f6b31, In Reviewf5ebd84): 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*_refcolumns is now partitioned by capture into a_namespace_part/_name_partpair 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.QualifiedNameGrammarlives ingraphitron-modelbeside 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_pathbecomes the child relationgraphitron_federation_key_field_segment,FieldSetGrammar.pathsreturns segments rather than dotted strings, and its tolerance contract is unchanged.graphitron_argument_path_segmentis 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 tographql_fieldthat 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 `CHECKbecauseCHECK (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 tooksql_constraint_columnout of the set, so eleven relations converted and not twelve. Because h2 rejects an insert that so much as names a generated column andFactSink.flush()named every column a relation declares, those eleven gained written statements inFactWrites(thirteennewRecordsites, conflict behaviour stated per relation instead of read off ajvm_/sql_name prefix), withflush()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 ofroadmap/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_claimandintent_name_matched_key_paircompare stored columns, taking the schema’s reducible budget of 30UPPER, 8POSITIONand 5SUBSTRINGto zero, 0 and 0, the residual 2POSITIONand 4SUBSTRINGbeing the bean-accessor rule and two recursion cycle guards that are not case folds at all.WrittenStatementCoverageTestgates 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 undermvn install -Plocal-db(14 modules, 0 failures), no code-string assertions on generated method bodies, user-facing-doc check clean over the one.adocfile, noRetired vocabularysection to sweep though the retiredfield_pathwas 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, thirteennewRecordsites 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('.') > 0tonamespace_part IS NOT NULLandUPPER(COALESCE(a, b))toCOALESCE(a_upper, b_upper)onUPPER(NULL)being NULL. One finding, not held against the gate because it postdates the delivery:intent_resolved_node_key_projectionlanded on trunk 91 minutes after this item entered In Review and brought two further per-rowUPPER, so the schema holds six and not the four R724 andfact-model.adocboth describe as the single survivor. Unlike R724’s hedge that comparison matches an authoredargMappingsegment 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 tointent_spelled_tableandintent_column_match_claimcomparing underUPPER()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 passa50b442; third In Review → Ready gatee0736f8, 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@serviceparam 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 nestedInputBeanleaf; 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@nodeIdwhose named member does not exist rejects, naming the directive and the missing member, so a typo never becomes silently-different behaviour.@tableon a grouping input stays inert (the type flattens exactly as its directiveless twin). D1:CallSiteExtraction.FieldBindingandValueShape.FieldBindingreplacesdlFieldNamewith an ordered non-emptyList<String> accessPathplus amapKey()accessor returning the last element (the wireMapkey;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.buildInputBeanBodyfillssdlByBindingKeyby 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 structuralRejectionnaming the dotted path: cyclic grouping (SDL-type-nameexpandingset, 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 oneMap<?, ?>descent local per distinct group prefix, parent before child, via aninstanceofpattern with an empty-map default, so no per-field arm needs a null guard and an absent, null, or non-Mapgroup yieldsnullfor 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 indocs/manual/how-to/handle-services.adoc, distinguished from theargMappingdot-path (which rebinds method parameters, a different axis). Five new loud JavaBean-arm build failures, deliberately: todaybindJavaBeanskips 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@nodeIdwhose 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’sMap<String, Object>parameter forced an unchecked cast at its call site, a hard failure in a consumer build under-Werror; widened toMap<?, ?>withinstanceof-pattern narrowing at every call site, the plural helper’s per-element cast wildcarded and its@SuppressWarningsdropped. That rule is stated onInputBeanInstantiationEmitter.buildSingularHelper/buildPluralHelperand enforced bygraphitron-sakila-example’s `-Xlint:all -Werrorcompile of the emitted tree, whoseFilmReviewGroupedfixture 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: pipelineInputBeanGroupingPipelineTest(16 cases, both arms: accept cases asserted structurally on the resolvedValueShape.FieldBindingaccess paths and leaf shapes, rejections by message substring, no generated-body string assertions), compilation tier via the depth-2FilmReviewGroupedfixture against a real consumer record at<release>17</release>(parent-before-child descent order is a compile error there), execution tier viasubmitGroupedReview_flattensTheGroupOntoTheBeanround-tripping a present group, an absent group per level, and a matching-member group still arriving nested. Retired:sdlFieldNameon bothFieldBindingencodings (survives only on out-of-scope carriers:SetColumn,KeyColumn,GraphitronType,PayloadConstructionShape,InputRecordShape,PayloadSdlField, the walkerContributionrecords); the sweep also caught abindFieldlocal that held a dotted path under the retired name, renamedfieldPath. 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-Werrorenforcer and approved (full reactor green undermvn install -Plocal-db, 14/14 modules,InputBeanGroupingPipelineTest16/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 (
95df965the delivery; Backlog968592d, Speccb11feb, spec revisiond4f78a6, Spec → Readyc07c57b, Ready → In Progressd7dc3e0, In Reviewb910259): 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_COLUMNSat all; the metadata existed only as a live reflection result behindJooqCatalog.nodeIdMetadata, which answers a validated question, so a malformed constant was indistinguishable in the store from a table publishing nothing. Two base relations in thesql_family now transcribe them:sql_node_metadata, keyed onsql_table’s own key with a closed form arm per constant (STRING/NULL/OTHER/ABSENT` andFIELD_ARRAY/NULL/OTHER/ABSENT) plus the stated value and the deterministic class-FQN witness, and its orderedsql_node_key_columnentries 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, andCHECK (NOT (type_id_form = 'ABSENT' AND key_columns_form = 'ABSENT'))keeps "no row" meaning "publishes nothing". Well-formedness moves out of capture intointent_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_UNRESOLVEDmirrorsfindColumn’s two-tier case-insensitive spelling against `sql_column.jooq_nameorcolumn_namerather than the SQL tier alone.JooqCataloggainsNodeMetadataFactsandnodeMetadataFactsOfbeside its four existing capture-facing siblings, withStatedConstantand one sharedgetFieldprobe so the reading half and the reduction cannot disagree about whether a class declared anything; reflection and raw-Objectinterpretation stay at that one containment site. Thesql_family charter is amended in the same change, having said "database" while this is the first whole relation of generated-model facts under it, wheresql_table.class_fqnhad 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 asCHECKconstraints, entry parentage and position density asFactSchemaGateTestcases, both relations inFactCaptureAgreementTest’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 (nodocs/changes, no declared retired vocabulary). Two sub-threshold notes recorded rather than filed:NodeMetadataFactsis the one capture-facing sibling without a compact constructor, so its null-tolerantkeyColumnNamesis unmodifiable only whenreduceNodeMetadatabuilds it, whereIndexFactscopies unconditionally; and the agreement assertion’sMALFORMEDarm 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 (
4085a53the delivery,3b33f2ethe follow-up item’s id; Spec49b734b, two spec reviewsa99e24c`+ed1a601, Spec → Ready `86229aa, Ready → In Progress7467601, In Review53901b9): multi-parentNestingFieldsharing admits the two alias-projected leaves, and the remaining BatchKey deferral stops being an open question. A plain-object nesting type declared on two@tableparents has each parent classify the shared type’s leaves against its own table (the anchor), andGraphitronSchemaValidator.compareNestedFieldsShapechecks the results agree; arms existed forColumnBackedField,TableField(R23) andNestingField, 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 ifisNestedWireableLeafadmits it at nested depth at all:ComputedFieldandColumnBackedReferenceField(both admitted at nested depth by R645) and theSourceShape.Tablearm ofBatchedTableField. Every otherBatchKeyFieldcarrier is rejected per-variant first byvalidateVariantIsSupportedAtNestedDepth(the Record-sourcedBatchedTableFieldarm,ServiceTableField,ServiceRecordField,BatchedPivotField,BatchedInterfaceField/BatchedUnionField, andBatchedTableInterfaceField), so it never reaches this gate on its own account. The first two admit on R23’s own argument and needed no emitter work:ProjectionCommandsmints the projection unit per anchor and its$projectreceives the anchor’s own table local, while the shared<NestedType>Fetchersregistration 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 onvalidateNestingParentCompat’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 `ResultKeyAliasedFieldcapability rather than on two class identities, and its two non-obvious constraints are both reified rather than remembered. It sits below theTableFieldarm becauseTableFieldis a member too, so an arm placed higher would swallow it and silently stop comparingfilters()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.PivotFieldis 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 addressedpivotUnit(parentTypeName, fieldName)and a nested leaf’sparentTypeNameis the nested type, so its address does not carry the anchor.isNestedWireableLeafkeeps 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 driftdevelopment-principles.adocnames. The arm comparesdomainReturnType()as the read-side analogue of theColumnBackedFieldarm’scolumnClasscheck, plus, on the reference half, thejoinPath()terminus read straight off the model throughJoinStep.HasTargetTable(this validator holds no catalog, andServiceCatalog.terminalTableForReferenceanswers 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, whichdomainReturnType()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, becauseOutputField.peelToClassNamedoes not unwraporg.jooq.Field, so every instance answers the constantPlain(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-deferredjoinPathderives from the single SDL declaration on the shared nested type. Also here: the class-mismatch check routes aResultKeyAliasedFieldmembership split through the class-carryingRejection.deferredoverload instead ofstructural, worded on the FK-orientation fact that actually differs (the only route there is an@nodeIdFK-mirror collapse landing on different sides per anchor, and the reference side always co-rejects on its ownNodeIdEncodeKeysdeferral, 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-sourcedBatchedTableFieldarm on both authoring shapes (a plain@splitQueryand a lookup-keyed instance, sinceisNestedWireableLeafgates onsourceShape()and reads nothing aboutlookup()). That deferral stays and becomes the documented end state rather than an open question:GeneratedUnits.rowsMethodaddresses the rows method on the nested type so every sharing parent mints the identical reference and the coordinate gets one DataFetcher, whilederiveSplitQuerySourcereads 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 `PivotFieldtoo. Coverage:NestingFieldValidationTestgains 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 offColumnBackedReferenceField(now admitted) onto a Table-sourcedBatchedTableFieldso the message keeps a live pin.GraphitronSchemaBuilderTestpins 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 ofFieldBuilder.classifyChildFieldOnTableTyperewraps a nestedUnclassifiedFieldthere, with the consequence asserted deliberately: the rejecting parent’s field is no longer aNestingField, so the shared group drops to one member andcompareNestedFieldsShapenever runs on that schema. Execution extends the R23OccupantLocationfixture (shared byCustomerandStore, both FK toaddress) with one leaf per admitted class, a{table: "address"}-entry scalar@field+@referenceand an@externalFieldwhose helper takesTable<?>(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@classifiedcorpus verdict flipped, as expected for a validation-time gate. Independent-session In Review → Done review: full reactor green undermvn 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 gitignoreddocs/target/stagingrender and the historical R23 changelog entry), and nodocs/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$projectselection gate switches on graphql-java’sgetFieldsGroupedByResultKey(), 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 namedoccupantDistrict/occupantAddressIdto keep this item’s shape off that defect rather than re-pinningProjectionSqlBaselineTestonto a spuriously projected column. -
R704 (
e1d9df5Track A slices 1-3/5-6,c04ffc2pagination,482e8a8routine catalog facts,306972cchain terminus + name-matched hop arm,59a746b`+fb851b1` the return binding,a302874the keying rule as a relation,68c639fself-review; Ready36a346a, In Review1d9d71c): The@routineread 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@routinefield silently dropped@defaultOrder, deferred@orderByand@condition, rejected@asConnection, demanded a restated@tableon 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 throughresolveTableFieldComponentsagainst the chain terminus, with the routine’s own IN-parameter arguments excluded off the resolved bindings rather than off the directive text;validateListRequiresOrderinglost itsChainexemption 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 oneTable<?>local the FROM, the seek’s source and the connection carrier’s count source at once, sototalCountcounts 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_routineandsql_routine_parametercapture the catalog facts (jOOQ generates noRoutineobject 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_terminusstates where a chain lands and on what kind of table;intent_routine_return_bindingandintent_resolved_type_bindingmake a routine’s result bind its return type at type scope, with disagreement stated ascandidatesrather than picked;intent_name_matched_key_pairstates the keying rule catalog-only, and the hop view’s newNAME_MATCHarm is its first reader. The classifier half sits at the producing edge beside the carrier’s own fixed point (RecordBindingResolver.groundRoutineReturnTypeplusTypeBuilder.routineReturnVerdict), because minting the binding globally would collapse the forkclassifyMutationRoutineCarrierdecides 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,ActorFilmderived andTilgangwritten, so the execution tier proves them equivalent against a real database. Independent-session In Review → Done review: full reactor green undermvn 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 theChainexemption failsValidateListRequiresOrderingPipelineTest.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 inQueryFieldandRoutineResolutionstill naming@tablewhere the code holds the binding,bindReturn’s rejection forking on whether `@referencewas 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 namesrolle, 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@referenceis deferred outright.BuildContext.synthesizeNameMatchedJoinandBuildContext.deriveRoutineCarrierPairsare 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 overgraphitron_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_hopalready enumerates every hop a path element can express andintent_field_reference_step_targetalready 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 joinsql_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.checkCarrierInvariantpairs a non-emptyjoinPathonly with a hop-anchored correlation, while the carrier’s is the hop-lessOnLiftedSlots, 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:
@defaultOrderon 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@routineread surface and diagnosed the shape behind it precisely: four independent read-surface axes governed by one boolean predicate inRoutineDirectiveResolver.orderOrConditionDeferral, one four-way conjunction inQueryField.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,@orderByand@asConnection, and refuses the return type any spelling but a restated@table. Second, and decisive, R659’s premise that@orderByand@conditionare honest capability deferrals is false. The render layer is already generic over both:OrderingBlock.declareSortViewis total overOrdering.ColumnsandOrdering.Helper,RootLauncherRenderer.conditionStatementalready treats an absent WHERE as data,routineBodyalready composes a.where(...)from hop filters, andOperationMember.Condition.OnReturnTablecarries a plainTableRefwith 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’sdepends-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 (
6a7b22dthe delivery,efd37f3the second-pass rework; Spec → Ready on the fourth independent Spec review804af44, In Review → Ready813ce39, Ready → In Progressd453e67, In Reviewbab8626): an@nodeId(typeName: X)argument or filter input field whose containing table reachesX.table()through a foreign key targeting columns other thanX’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 `EXISTSover the FK, the exactBodyParam.RemoteColumnPredicateshape a joined plain@referencefilter already emitted.EXISTSis 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@lookupKeyrails 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-joinPathsentinel onArgumentRef.ScalarArg.ColumnBackedArg, aDirect-vs-NodeIdDecodeKeysextraction test inFieldBuilder.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 sealedFilterBindingwith armsLocal(List<ColumnRef> ownTableColumns)and a payload-freeRemote; it replacesliftedSourceColumnson both reference carriers and joinsColumnBackedArgbeside itsjoinPath, sowrapIfRemoteis the one discrimination andremoteIfReferenceJoin/translatedFkRejectionare gone.Remoteis payload-free because in both cases reaching it the terminal tuple already is the carrier’scolumns(), so onlyLocalcarries a tuplecolumns()cannot supply;joinPathstays deliberately orthogonal, since aLocal-bound FK-target with an authored@conditionstill needs the path for theFkTargetConditionFiltercorrelation. The invariants live on the carriers, not on the payload-free arm, which is the only place that can see the path:Remoterequires a non-emptyjoinPath, and onColumnBackedArg(where the local tuple restatescolumns()) 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.admitMutationInputFieldsdispatches on the carrier without reading any binding-sensitive slot, so it would have silently admitted aRemotecarrier 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 byFilterBinding.remoteBindingUnsupportedrather than oneRejectioninstance, because the walkers' channel is aRejection.AuthorErrorsibling aDeferredcannot inhabit; write-side emitters read their columns through oneLocal-destructuring accessor that throws onRemote, so a bypassed gate fails loudly instead of emitting a wrong statement. Execution fixtures landed in thepublicschema as the spec decided, costingCREATE TABLE`s plus seed rows in `init.sql, twoNodeIdFixtureGenerator.METADATAentries and SDL in the example module’s existingschema.graphqls, with no new Maven execution,jooqPackageor.graphqls:xlat_parent/xlat_childand the compositexlat_comp_parent/xlat_comp_childwhose child FK targets a different composite unique constraint. Coverage isTranslatedFkTargetRailGatesPipelineTest(all four gates plus a direct-FK carrier that must still be admitted, asserting the shared text rather than four substrings), the two rewrittenNodeIdPipelineTestcases now asserting the carrier shape and the one-hop predicate, the two carrier-invariant tests, andTranslatedFkTargetFilterExecutionTest(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-@referenceargument arm passedRemoteunconditionally on a precondition it had not checked, and since@reference(path: [])is legal SDL and an argument site callsparsePathwith a null target table, the empty path survived, the column resolved against the field’s own table, and the carrier’s ownRemote-needs-a-path invariant threw out of classification: a regression from a working bare localEqto an untyped crash. The invariant was right and the call site was wrong, so the arm now forks on path emptiness and bindsLocal, mirroring the input-field sibling inBuildContext.classifyInputFieldwhose divergence was the tell; both surfaces are pinned inReferenceFilterRemoteColumnPipelineTestbeside the direct-FK-stays-local guard, so the matrix reads that the binding follows the path in both directions. Whether an inert element-less@referenceshould 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 undermvn 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’sSQL_LOGtoken cross-checks are the sanctioned structural form the test-tier guide names, beside real row assertions); user-facing-doc check clean, the onedocs/change being the widenedcode-generation-triggers.adocrow; retirement sweep clean across all eleven declared terms, with the surviving encode-sideJOIN-with-projectionprose andliftedSourceColumnsonDirectFkverified 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 atFieldBuilder.java:1628and:1645, the patternremoteBindingUnsupportedwas 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 (
d048054implementation; Ready → In Progressc487906, In Progress → In Reviewa193eeb): 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 mintsChildField.BatchedTableInterfaceField(in theTableTargetFieldseal, implementingBatchKeyFieldand decliningParentRowDemandbecause the batched arm ofProjectionCommands.tableTargetContributionprecedes the demand arm and both answer the same column list), single cardinality keeps the per-parentTableInterfaceField. 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 `TableBackedrather thanTableBound, since a joined-table discriminated interface populatesJoinedTableBoundexclusively and porting the sibling’sanyMatchverbatim 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.@splitQuerybecomes 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 newsplitquery-redundant-on-discriminated-interface-childlint rule with the computable delete fix, emitted through awarnIfSplitQueryRedundantgeneralisation 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.DiscriminatedCorrelatedChainis a thirdCorrelatedarm carrying theDiscriminatedTablepayload 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), andprojection()moved offCorrelatedonto a newCorrelated.Projectedsub-seal so the member stays total where it is declared;BatchedRowsFragments.discriminatedBodybinds the shared prelude, parent-input attach and WHERE fold toDiscriminatedTableFragments.projectionplus a newly extractedjoinedDetailJoins. NoalwaysProjectcolumn 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 andDeliveryFactRelation.mint, where the new arm precedes the marker reads so the redundant marker cannot claim the trigger at either cardinality;singleTableBackedVerdict’s `falsecase stands with its rationale rewritten. Two hazards settled by hand that no gate would have caught: the loader registration routes throughTenantDslEmitter.loaderNameDeclaration, because a batch loader resolves oneDSLContextfrom 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, themediaListverdict tuple in thetable-interfacecorpus 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 inDeliveryFactPinTest(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.adocgains the author-facing bullet on the redundant side of the line that page already draws, andsupported-schema-shapes.adocregenerated. Independent-session In Review → Done review, one pass, approved clean: full reactor green undermvn 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 thehasConnectionSplitFieldgate 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 intorenderas the emitter families migrate onto command relations, and R682’s census puts the fetcher family (this file plusFetcherEmitter) 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 assource-orientation-javadocs.md’s partial-mitigation option, and the R6 `FieldBuilderprecedent 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 since456ee94. 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 (
0bf512droot-only scoping,33218aarejection 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, holdingnullwhere the key matched no row. The manual had documented that positional contract for as long as both had shipped while the generator inner-joined theVALUEStable and let a missed key contribute no element, sofilmById(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 `idxas__idx__and scatters through a newscatterLookupByIdxhelper, which is also what carries input order, so the arm emits noORDER BY, and the launcher’s value type becomesList<Record>because a jOOQResultcannot hold a null element; two rows landing on one key keeps the first rather than throwing asscatterSingleByIdxdoes, 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, soGraphitronSchemaValidator.validateRootLookupgains a nullable-element rejection and every existing root lookup declaring non-null elements has to drop the inner!. Root-only throughout: a child@lookupKeycoordinate narrows each parent’s list, keeps.orderBy(input.field("idx")), contributes no element for a miss, and correctly keeps[Actor!]!. Coverage:ScatterLookupByIdxTestdrives 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 absentORDER BY; execution pins thatlanguageByKey(language_id: [2, 1, 2, 99, 1])answers2, 1, 2, null, 1and 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@lookupKeyshape while describing only the root arm, then the@asConnectionsentence attributing one arm’s error message to both. The third gate also removed three code-string body assertions the item had added toRootLauncherRendererTest, 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@lookupKeyplus@asConnectionreports a@tableerror 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
@asFacetthe 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 noGraphQLSchemacannot 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 andgraphitron_type_declaration_synthesisprovenance), 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..935c964implementation; reworkf04b1f2/2320aa4; held at the gatef65cd3f): 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 throughServiceCatalogat build time and called directly, the contract being a signature (exactly one seam parameter typedorg.jooq.Configurationorjava.sql.Connectionanywhere in the list, the rest is payload, the return type is the handle), which jOOQ’s own generatedRoutinesexecuting methods satisfy as-is, so the headline path is zero hand-written Java and the seam rule doubles as the overload selector against jOOQ’sField-expression siblings. Deleted rather than carried: the<variables>Postgres sugar and with it the runtime’s non-Postgres dialect guard and thesession-state-convention-fencelint rule (a build cannot grade a reflected method’s position on the integrity gradient, so the gradient moves todocs/security.adocwhere it is explained instead of asserted);<handle>(the return type states it);<stateSurvivesTransactions>and the whole per-settle re-fire, becauseacquireasserts autocommit before the mount, so the mount is its own committed transaction and no later settle can revert it, which also takesGraphitronTransactionProvider’s `priorAutoCommitfield and second constructor; the generatedSessionHookinterface, its no-op, the runtime’s hook field andRuntimeHookProjection, replaced by one final class with staticmount/unmountand 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@servicesite entering once, type disagreement caught by the classifier’s existingTypeConflict), reversing an earlier disjoint-namespace decision;CLAIMS_KEYgoes with it. Two structural moves carry the rest: acquisition is lazy on every path through one unified per-key carrier (ConcurrentHashMapkeyed byOptional<tenantKey>, single-tenant as the one-key case, the whole(connection, dsl, handle)entry minted inside onecomputeIfAbsent), 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-tenantConfigurationto ride; and the runtime’s source SPI takes aDataSource/SQLDialect/Settingstriple 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$sessionargMapping sigil (absorbing the former R640 and dissolving its four forks: ordering, staleness, fan-out scope and opt-in surface), owned byArgMappingSigilwhich both the build-side parse and fact capture route through, landing in a siblinggraphitron_service_arg_mapping_sigilrelation rather than quarantining as undecoded overflow; a$session-binding field under a tenant context classifiesInheritedso it reads its own tenant’s handle. Five newReflectionErrorpermits with theirtyped-rejection.adocparagraphs; the provenance family collapses tostore_graph_session_mount/store_graph_session_unmountwith the foreign key making "unmount without mount" structural. Coverage:SessionHookImplGeneratorTestandTenantConnectionsGeneratorTestcompile the real emittedTypeSpec`s and drive them (no generated-body string assertions introduced); execution-tier `SessionHookExecutionTestproves the direct-Routinesand facade shapes indistinguishable at the database, RLS scoping across a mutation’s commit and rollback on anautoCommit=falseDataSource, wholesale overwrite, fail-closed throwing mount, and the contextArgument unification end to end;TenantFanOutExecutionTestproves two tenants read two distinct handles in one operation. Independent-session In Review → Ready → In Review → Done review; both passes green undermvn install -Plocal-db. The first pass held on one blocker: the item specified a located runtime throw for a$sessionread on an escape-hatch operation, and what shipped was an unguarded cast bindingnullinto a service parameter meant to carry mounted identity. The rework routes every$sessionread through a generatedTenantConnections.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 thejvm_method_parametercensus debt plus the$sessioncompletions follow-up named in their LSP items. Absorbs R640; feeds R468. -
Discarded: Support
@asConnectionon 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@referencejoin 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 noDataLoader), both routes with the rejection argument, the unpinned child deferral in its retired vocabulary, and the seam question as open rather than settled, since theprojection/joinedStepsplit was designed for the rejected route. This item’s third open question, that the interface arm never readsforcesSplitDeliveryso@splitQueryis 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 atdocs/history/road-to-the-relational-core.adoc, alongside a newdocs/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 whileroadmap/changelog.mdkeeps the ids; and the page defers to the present-tense pages underdocs/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 thegraphitron-rewrite-historybranch, which is the only copy of that history in existence. Re-open trigger: none as an item; further history pages go underdocs/history/as ordinary docs work. -
R629 (
fe5f35bimplementation; Spec → Ready3499425, Ready → In Progress368343c): mountable GraphQL-over-HTTP delegates with an explicit operation policy.graphitron-jakarta-rest’s endpoint surface splits out of `GraphqlResourceinto two public@ApplicationScopeddelegates a consumer resource can mount at any path, template parameters included:GraphqlHttpHandlercarries the whole decode/decide/execute/encode pipeline with every Jakarta REST input passed as a parameter rather than injected, andGraphiqlBundlecarries the self-hosted page and its assets (the{{ASSET_BASE}}rewrite now resolving against whatever path served the page). The GET-mutation guard generalises intoOperationPolicy, a final class with three validating factories (queriesOnly(status),queriesOnly(status, message),allowing(Set, status, message)) publishing onlypermits; 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’sInvalidSyntaxresult, not the pipeline’s parse-failure wording); GET is routed with the package-privateSPEC_GETand the nullable-guardexecuteis 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.GraphqlResourceis now five annotated methods over the two delegates plus a throwingrequireBuiltInEndpoint()gate (throwing becauseschema()returnsString, so no published return type widens for the gate’s sake), backed by a new defaulted SPI toggleGraphitronApplication.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-exportedGraphqlResource.GRAPHQL_RESPONSE_JSONstays a constant expression (verified in the class file), so consumer@Producesannotations naming it keep compiling. Coverage splits by tier rather than habit: unit-tierOperationGuardTest(12 cases, no container) pins the guard as a decision table overresolveOperation×permits, including theoperationNamesmuggling case in both directions, both unresolvable-fall-through cases, the defensive set copy and all four factory validations; execution-tierMountedEndpointTest(12 cases, over a/env/{callingEnvironment}/graphqlfixture 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, aNotAuthorizedExceptionpassthrough, and all five built-in routes answering 404 with the toggle off;OverlappingMountTest(3 cases, neutral/probepair, 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 isGraphQLOverHttpConformanceTest.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.htmlhead comment, bothtools/graphiql-build/README.mdoccurrences). Independent-session In Review → Done review; full reactor green undermvn 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 retiredGraphqlResource.execute(), repointed atGraphqlHttpHandlerin the approval commit; below the recurrence bar for `RetiredVocabularyGuardTest’s registry. -
R589 (
b97b1cbumbrella amendment,882c037claim views + conflict detection,fac7704column-match witness view,15d44b8demand/exemption shadow rows,beafe4acarrier split + funnel mints,d20e9ccConflicted projection,d5cec11+3f53bab+7aefee2review fixes): classification is a relation; validation adds facts. Theintent_derived stratum arrives:intent_authored_field_claim/intent_authored_type_claimunion one arm per claiminggraphitron_relation (position masks in the arms, presence fallbacks where a decode can decline), andAuthoredClaimConflicts, 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_claimproves the inferred-claim witness model in shadow; the demand and exemption rules land as rows (intent_*_demand*/intent_*_exemption*,ReachabilityRowsmaterializing the type domain at capture cadence) with corpus agreement against the legacy registry.InputField.ConditionOwnedFieldsplits out ofUnboundField(which becomes the genuine-miss carrier,attemptedColumnNamenon-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.Unclassifiedsplits intoUnresolvableandConflicted(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), withConflictededge-bearing throughTableClaiming. Accept/reject is unchanged throughout; retired:PairVerdict/pairVerdict/reduceDirectiveConflict, the per-position detector sites, the three-cases-in-oneUnboundFieldreading. -
R613 (
19dec9acapability,be7c128docs): 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.requireNoGeneratedFilterOnLookupwith thelookupboolean that existed only to reach it, and the lookup arm ofGraphitronSchemaValidator.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:validateRootLookuphad been OR-ingLookupMapping.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.actorsGeneratedinline multiset,Film.actorsBySplitLookupGeneratedbatched loader, the latter two asserting the rendered predicate and row content since they mint runtime-prefixed aliases), andConditionCommandsPipelineTestpins the key-not-restated fact at the model. Docs:lookupKey.adocgains 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@lookupKeyon an individual input field as R614. -
R603 (
79b60a2core, gate rework76ac937; re-baseline onto the shipped R610 stored0dec98, Readyd157d0d, In Progress51b2fc0, In Review6450d93, gate rework requested79d05b5, rework In Revieweb6a328): The fact store gains its sixth family and its first post-capture oracle writer.javac_diagnostictranscribes a dev-loop compile round injavax.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 anysql_orjvm_row; what distinguishes it is not the kind of row but when its writer can run. So there is nooutput_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_nameleads the natural key(file, line, column, ordinal)with a structural FK tostore_graph, inheriting R610’s dimension by definition and passingFactSchemaGateTest’s exemption-polarity gate with no case to make; `kindstays an open column because aCHECKenumerating javac’s enum would be a hand-maintained copy of an externally owned taxonomy,codeis 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 thanNULL, a primary-key column admitting none, with the departure from the NULL-uniform absence discipline argued in the comment rather than drifted into.CompileDiagnosticreshaped while open:severityrenamed tokind(it always heldDiagnostic.Kind.name()), a nullablecodecomponent added, andfromnormalises 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 ownseverity()projection, read byCompileRound.errors()and the MCPdiagnosticstool alike.CompileFactsis 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 (DevMojoopens one at startup and closes it incleanup, 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.StoreRefreshneeded 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 carryingGRAPH_NAMEis ownership-scope-cleared by construction. The agreement driver gained anORACLEarm 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. TheDiagnostic.Kindpartition 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 newKindwould 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 indevelopment-principles.adoc§ "Documentation names only live tests/code" when the named test did not carry the claim. Separately,CompileFacts.javahad 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 onKind.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 mintedstore_graphanchor’slast_capturednow 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 theDataAccessExceptionwrapper 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 undermvn 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 thediagnosticview’s compile arm readsjavac_diagnosticfrom 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 (
b534810core, gate reworke344cac; Spec280e34eafter nine revision passes, Readybc3cdaa, In Progress945cadc, In Review652e105, gate rework requestedab51049, rework In Reviewe6d7fd4):graph_name VARCHAR NOT NULLleads the primary key of everygraphql_/graphitron_base relation (83 of them), so one fact store can hold several graphs without fusing them. The motivation is federation, whereUserin 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 leavestarget/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. Newstore_graphanchors 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_andsql_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 theclaimkeys so no SDL call site changed; that is the load-bearing half, sinceclaimmirrors 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 bygraph_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:openReadOnlyand its copy-to-temp snapshot deleted rather than converted (H2 refusesAUTO_SERVERwithACCESS_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 agraphitron.store.directoryproperty, both documented inmojo-configuration.adoc, plus the workspace resolver that chainssiblingModuleBasedirs’ existing one-step aggregator walk to the outermost root (the filesystem aggregator chain, since `MavenSession’s top-level project answers "where was `mvninvoked" and the parent chain is a different graph than the aggregator chain). Three gates joinedFactSchemaGateTest, 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 tostore_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 departuresql_referential_constraint.referenced_source_namedeclares a foreign key that can cross package partitions, whileCatalogFactCapturecleared each package’ssql_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 themultischemafixturecatalog, not inferred. It had gone unnoticed becauseFactCapture.runcaughtDataAccessExceptionand 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_stampcompared to themselves, and an FK closure counting graph-free edges),SchemaRecipe.buildFilenormalized 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 undermvn install -Plocal-db(14/14 modules), the newWarmStartRefreshTestcase drivesFactCapture.capturedirectly 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 undertarget/. Deliberately deferred with their first readers:store_graph_sourcemembership for thejvm_/sql_side, eviction (this item writes thelast_captured/last_seenstamps 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 reworkdd77f66; Spec11de611after six revision passes, In Progress86b4db2, In Review0087b5b, gate rework requested6fc2ca1, back to In Review177e269): AnID-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 anIDmeant, an author could not tell the two readings apart by looking, andBuildContext.resolveDecodeHelperForTablehad no safe deletion point. The grammar makes the implicit reading available in exactly two places where it cannot be ambiguous:Node.idon a node type is that node’s identity by construction (already shipped as R580’s carrier, andtypeName:is now rejected there as either a restatement or a contradiction), and an argument or input field named for its target’sNode.idis 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@nodeIdgeneralised to every coordinate as the middle tier ("node id, target inherited"), which the shipped@nodeIddocumentation already promised and which@nodebeingon OBJECTmakes forced rather than convenient; that generalisation dropped the fixture migration from ~25 sites to zero. The argument coordinate resolves throughNodeIndex.forNameoff the unwrapped element type; the input coordinate holds no type name to discard (InputFieldResolver.resolvethreads the input type’s own name, and a mutation input has no node type in its SDL at all) so it resolves throughNodeIndex.forTable, singleton or a rejection namingtypeName:. 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,findGraphQLTypesForTableand thetypeNamesByTableKeyindex behind it, both shim loggers,CallSiteExtraction.NodeIdDecodeKeys.SkipMismatchedElementwithLookupRows’s effective-row machinery, and two shim-intent test classes; `resolveDecodeHelperForTypecollapses to theforNamelookup and returns anOptional. Two departures from the plan that read better than it: the rejections landed classifier-side rather than inGraphitronSchemaValidator(the classified model exposes noschemaType()at those coordinates, andUnclassifiedFieldalready carries the source location the LSP reads, pinned by a test), and rule 6’s key columns come off the classifiedNodeTyperather than the table’s raw metadata, so SDL keeps winning ontypeIdand key order. Two fixtures turned out to carry@nodewithoutimplements Nodeand had never beenNodeType`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 itsGraphQLQueryTestround-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 fromfieldDef.getType()while every caller ofresolveTableFieldComponentspasses the connection-unwrapped element type and a matchingrt, so on an@asConnectionfield 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 returningBazConnectionresolved silently onto the rawidcolumn, 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: themigrating-from-legacyrecipe 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, andSkipMismatchedElementhad 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 throughclassifyArguments/classifyArgument(one caller, so the thread covers all tenresolveTableFieldComponentssites), 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.sqlstill documents the liveidreffixtureDDL as existing for the deletedIdReferenceFieldshim tests, misdescribing a fixture whose actual consumers are now the qualifier-map and@reference(key:)tests) and R616 (theImplementationsection’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
@nodeIdandIdReferenceFieldsynthesis 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-qualifierIdReferenceFieldsynthesis and the bare same-table arm inBuildContext.classifyInputField, and the non-idbare-scalar-IDarm inFieldBuilder, together with both shim loggers,resolveDecodeHelperForTableand the helpers that died with it. The retirement could not be sequenced the other way round: while a directive-lessIDcould 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 (
cefb16aclassifier +8cf7766docs, gate rework47d11a4; re-scopedd9b5616, spec body81afbee, Spec revisions9a55d9e`+6b905c1`+b98be81`+3bb3a82, Ready `89fa452, In Progressbd1d6d1, In Review8ad26c5, gate rework flip6b2f808`+7eee77b, rework In Review `b886e1c):implements Nodedeclares nodehood and@nodesupplies or overrides the two identity parameters, so a@tabletype publishing the Relay contract over a jOOQ class carrying__NODE_TYPE_ID/__NODE_KEY_COLUMNSis a complete node declaration and takestypeIdandkeyColumnsfrom the catalog. Unchanged on every other axis:@table+ metadata without the interface stays aTableType, which is what keeps a nesting projection over a node-bearing table from becoming a second node; explicit@nodestill wins on the axis it names;@nodewithout 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 toUnclassifiedTypeby the typeId-uniqueness check. The predicate split is the real scope:@nodepresence andNodeTypemembership used to be the same set, so several consumers read the directive off SDL and stayed consistent by coincidence, and newNodeDeclarationnames the question once for reachability seeding, the arrival fold, federation entity synthesis and the LSP node view. It sits above classification and takes only aJooqCatalogbecause two of those run before any type is classified.NodeProvenancerecords 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@nodepath, not a compatibility rider: a node type whose table also has a column literally namedidsilently 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 (@nodeIdfor 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_COLUMNwas removed rather than kept for theSource.CLASSIFIERcensus, sinceLintRule.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@nodeIdor@field, each behaviour-preserving against the shipped warning, and none ingraphitron-sakila-example. The hoisted arm is pinned to the Node-interfaceidfield rather than the shim’s any-bare-IDpredicate, which would have reroutedexternalId: IDon 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 addingimplements Nodeto 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’sFilmActorconverted to the inferred spelling rather than a variant added, so the existingfilmActorByNodeIdround-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’sLintFixis 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: noClassifiedCorpussibling (the verdicts are pinned byNodeInferencePipelineTest, 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.adocstill 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, fixedcode-generation-triggers.adoc’s prose and variant-table row, and opened a `Named structural errorssection in the diagnostics glossary with the drift-protection paragraph amended to keepDiagnosticsDocCoverageTest’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),NodeInferencePipelineTest37/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 carryingimplements Nodeand@tableand no@node, grouped by the backing table’s__NODE_TYPE_ID, and confirm every group is a singleton and no member’sidis@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 `@nodewithoutimplements 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 (
be60ebacore,81d5073gate rework; filed453b916, spec revision819da51, Readyf3f9faa, In Progress18c8842, In Review1db2240, gate rework61b95d6, rework In Review00d8c18): 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, whichadoc-xref-section-anchor-gate.mdhad taken up 17 times. NewInertSpansowns the vocabulary as one unit:plusFormFitsis 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 `inlineMdToAdocpipeline 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.GeneratedAdocSpanGateTestrenders 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 theRetired vocabularydeclaration 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,titleLabeldropped the pipe escape it had inherited from a cell context it no longer serves,titleLabel_bareResidue_failsTheCorpusGatepinned 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-dbat 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-directiveintent_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 aWalkerResultwrapper), its operation axis was delivered by R563 asOperationMember, 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 inroadmap/audits/2026-08-06-r222-lineage.md. -
R584 (
076e9cc; filed3830a04, spec bodye430740, Spec4e40d1d, Spec revise2497c0c`+f91f027, Ready `d3f1524, In Progress4ca7647, In Review154ff7f): The MCP handshakeinstructionsstring routes an agent to every advertised tool family instead of one. The shipped file spent a full paragraph oncatalog.tables/catalog.describeand never named the other ten tools or thedirectivesresource, so agents re-derived from text what the wire already carried: shelling out topython3to count diagnostics whose pre-paging total was in the tool’s own summary line, and grepping a whole monorepo for DELETE mutations thatschemaalready classifies with a resolved table. The fix is a decision about what the slot is for rather than a longer list. Theinstructionsstring is ambient (received once atinitialize, 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, restatingcatalog.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.txtappended exactly whenexecuteConfig != null; mirroringexecute’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 `ServerInstructionsTestholds three assertions over one subject: a bidirectional coverage pin (advertised-is-routed, routed-is-advertised) deriving the advertised surface from a booted server vialistTools/listResources/listPromptsrather than a hand-written list, asserted against the ambient string per boot and againstdocs/manual/how-to/mcp-agent-context.adocat 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 sixMcpWire.pagetools atlimit=1and 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) soType.field,.graphqls,mvn graphitron:devandfqcn#method/aritydo not read as tool claims, with no ignore list.GraphitronMcpServerTestgains the composed-arm case; its existing verbatiminitializeReturnsBundledInstructionsnow 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 undermvn 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 ; droppingcatalog.describeandrecordsfrom the prose fails the forward direction naming both, inventing adiagnostics.aggregaterouting line fails the reverse, renamingedgesin the manual fails its forward half, and making the composition unconditional fails the no-database boot onexecute, 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 ofdocs.search/catalog.searchcarry no tool prefix or count, which is why convention 1 is scoped to the paged tools). R569 needs nodepends-onedge ; registeringdiagnostics.aggregatecannot 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, rework608fd85; Specf107ce4, Readybf0d649, In Progress72421e9, In Reviewe7c420c, gate rework21b0b699`+bbd829a6, rework In Review `63e164d8): Operations became a relation and the leaf cross-product dissolved additively.coordinate -> operationis the one genuinely multi-valued relation in the classification model, and storing it as a single-valuedOperationarm 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 carriedfilters/orderBy/paginationwhile theiroperation()switch mapped them to aLookuparm with one slot),Paginatefused three verbs into one record, a DML coordinate performed two operations and the model could name only the materialized one, and both structuralOperationconsumers 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 sealedOperationMemberfamily plus the member view derived from a compile-total 51-leaf crosswalk; the instruments (LeafRatchetTest, the member-grain census); the keystone, where five per-triggerFactVisitor`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 ofOutputField.operation()with its three leaf-identity switches; the back-half re-source, which materializedDeliveryFactand collapsedLauncherCommands.MINTING_KINDSonto a sealed launch verdict; three dissolution slices (the lookup triplet onto their fetch siblings behind a sealedLookupResolution, the DML verb split onto the sealedWrite.Dmlpayload with the dialect requirement becoming a derived accessor, the pivot and routine operation halves); the corpus voice, where@classified(operation:)became a required arm-grainoperations:multiset and the 17-armOperationseal 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, andLeafReconstructionKeyTestnow enforces the acceptance claimleaf = f(source, delivery, target)as a declared triple per surviving leaf, pinned total againstGeneratorCoverageTest.sealedLeaveswith 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.adocstill 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;LookupValuesJoinEmittersurvived 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 thedocs/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 theTableTargetFieldcensus matches the seal’s four permits exactly,LookupValuesJoinEmitterhas zero occurrences outsideLeafRatchetTest’s deliberate history lines, and the four test edits are comment-only. Trunk was red from R580’s `NodeInferencePipelineTestwhen 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--verifyexecution, so the partial-trace footgun that fired twice in this programme is still unguarded), R591 (SqlGeneratingFieldremains 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; Spec4e9c30d, Ready6e85bd9, In Reviewf5acb32): Dropped the deadparentTypeNameparameter fromFieldBuilder.parseExternalRefand followed the orphaned value one frame up, offExternalFieldDirectiveResolver.resolveas well, since that call site was its only reader. Four removal-only edits, no logic moved: the cascade terminates inServiceDirectiveResolver.resolveandFieldBuilder.classifyChildFieldOnTableType, where the name is genuinely live (parent-table validation and return-type projection on one side,UnclassifiedField/ComputedFieldconstruction on the other). No tests, by design: a signature narrowing has no behavioural axis, so the compiler plus the existing@serviceand@externalFieldpipeline 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@paramjavadoc, and every sibling resolver that keeps a leadingparentTypeNamewas checked to actually read it, so no parallel dead parameter escaped). The user-facing-doc check and retirement sweep both skip: nodocs/changes, nothing retired. -
R581 (
c87667b):@nodeId(typeName:)resolves off the named type instead of reverse-mapping its backing table. Declaring a second@nodeover 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.resolvesettled the type name, derived the target table from it, then askedBuildContext.resolveDecodeHelperForTableto map that table back to a NodeType, and that reverse lookup answers "zero or multiple" as soon as a second@nodecovers the table. Reported from the sis / utdanningsregisteret federation work, where the sharedOrganisasjonentity has to live alongsideURegOrganisasjonover one table; the second type needed no reference from any query, field or input, and declaring it was enough.BuildContext.resolveDecodeHelperForTypeis the new name-first entry point (NodeIndex.forNamewhen a NodeType carries the name, the table-keyed helper only for the orphan case it exists for), consumed byNodeIdLeafResolver.resolveandbuildInputNodeIdReference; the call sites that genuinely hold only a table name (the synthesis shims, the bare-scalar-ID argument arm inFieldBuilder.classifyArgument) keep the table-keyed helper, and the resolver’s surviving rejection is re-aimed at what is actually wrong at that point.resolveTargetKeysreorders the same way, so a named leaf takestypeIdandkeyColumnsfrom the type’s own@noderather than from the table’sKjerneJooqGeneratormetadata; the old order discarded the per-type reconciliationTypeBuilderperforms, which is what lets SDL win ontypeIdoutright and onkeyColumnsorder. 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 wholegraphitronmodule (reverting the name-first decode resolution fails the three new multi-@nodecases; reverting theresolveTargetKeysorder 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, bothid: ID! @nodeIdfields classify, andGraphitronSchemaValidatorreports nothing. Two findings filed as follow-ups rather than held at the gate: R583 (thetypeIdaxis of theresolveTargetKeysreorder is unpinned, and it is the wire-format prefixresolveNodeIdRecordDecodefeeds intodecodeValueson 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.adocinstead 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 (
e54be59core,870bcd1sweep,cd8538facceptance fixture,c757b0averb rebase; Spece31c51e, revisionsb77a5f4`+82a0dc0`+43e12c7, Ready1539675, In Progressedfb780, In Review88e7779): 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 newer10-SNAPSHOThit six type-level rejections at once.TypeBuilder.buildInputTypenow falls straight through to the plain path and the deprecation is announced per usage as aBuildWarning.NoRuleadvisory that says the directive was ignored, not only that it is deprecated: an input whose@tablenamed 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@tableon anINPUT_OBJECTwere harmless only because the rejection short-circuited them and all four went live the moment the input classified:RecordBindingResolver.groundRootProducers(an input-axisProducerBinding.RootTablethat honored the directive, deciding the backing carrier, and folded against a@serviceparam’s own observation into aRecordBindingMultiProducerrejection, so the build still failed with a backing-class diagnostic instead of a migration message),InputBeanResolver.collectJooqBindings(nested@tablerejected 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), andMutationInputResolver.rejectInputFieldDirectives(recursion gated the same way; this one failed open, letting a@conditionburied inside a nested@tablegroup 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 offOperationMember.Write.Dmlrather 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 frombuildInputType, so warning multiplicity is not a function oflookAheadVerdictmemo timing; fix-less by design, sinceNoRulecarries no fix field andLintFix.deleteBareAppliedDirectivegates 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 newTableOnInputIgnoredNestingTestfor the two nesting-path deletions plus the fail-open@conditionsite, the service-param and nested-input rejection cases inJooqRecordServiceParamPipelineTestrewritten 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!reachablefirst and would have passed for the wrong reason), andgraphitron-sakila-example’s `CityCountryFiltercarrying@tableagain as the end-to-end proof through the plugin, withFixtureWarningsGateTestasserting 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 “buildInputTyperejects any input carrying@table”, `code-generation-triggers.adoc’s `UnclassifiedType+ "build fails" row),directives.graphqls’s `@mutation(table:)outranking claim (nothing to outrank), andmigrating-from-legacy.adoc, where the location moved out from under== Hard removalsinto a retitled== WARN today, error laterbucket 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_typesanchor,TABLE_ON_INPUT_RETIREDcase,_rejectsAtTheTypemethod 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@lookupKeycase’s narrower claim that it survives the recursion gate alone). Follow-up R574 filed for four residuals the acceptance criterion did not reach: an unusedDIR_TABLEstatic import inInputBeanResolver, twotable.adocConstraints bullets that read unqualified but now hold only onOBJECT/INTERFACE, andmigrating-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 (theInputBeanResolverdesign 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; Specea87dc1, plan bodya26a5bd, provenance corrections2376e92`+cbc11bc, Ready `d92af4c): The javadoc{@link}reference gate survives an out-of-rangeproject.build.outputTimestamp.maven-javadoc-plugin3.12.0 bundlesmaven-archiver3.6.4, whoseparseBuildOutputTimestamprange-checks against a1980-01-01T00:00:02Zfloor and throws two seconds under it onSOURCE_DATE_EPOCH=315532800, the ZIP epoch NixOS exports fromstdenv; the property is never set here, butmaven-archiverfalls back to that environment variable, so thecheck-link-referencesexecution 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 bundledmaven-archiverto 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’spluginManagement, so thereleaseprofile’sattach-javadocsjargoal 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.outputTimestampwas 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 theDATE_MIN/DATE_MAXthrow, 3.6.5’s numeric branch returnsInstant.ofEpochSecondunchecked 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 recorde26e18d; Spec664b37e, revise3cf90c3`+1a6fd74, Ready `0650a27, In Progress74dc897, In Reviewd82b734): 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; theLauncherSource/LauncherResultprelude enums are mirror-tested against the sealed arm sets. Membership went producer-declared per the Spec’s review round 1:LauncherCommands.MINTING_KINDS, a sealedLeaf/DmlReturnkey (the DML family’s membership is return-arm-conditioned, a census-corrected refinement of the Spec’s flat leaf set) with the singlecoversaccessor owning the conjunction;mintedMethodOfandproduceWithoutSchemare-sourced onto it,LauncherMembershipTestbinds declared to observed minting in both directions over a fixture reaching all thirteen kinds, and the closure test’s hand-maintained twelve-armisCoveredFamilyMemberswitch retired. The invocation axis is not an argument (the axis census showed the source arm determines it):INVOCATION_BY_SOURCEis total producer data, pinned at the membership fixture, the closure run and the corpus sweep via the sharedLauncherAxisPins. Deliberate absences keep explicit negatives: the root@servicepassthrough 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 andcoversrather thanemitsKeyedReQuery()(true for an Encoded DELETE while no row mints; that predicate’s javadoc consumer list corrected). Coverage: theLAUNCHER_COMMITMENTobligation registered inobligations()andcorpusObligations(), all 15LaunchSource`+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, andLoaderDelegated’s biconditional entailment stated on the row. `QueryViewRenderer.INTERNAL_DIRECTIVESderived from the parsed prelude (the latent@synthesisesleak 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_REFERENCESratchet 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
b7032caplain root,d3178e8connection non-faceted,395b4c4facet plan + legacy connection builder retired,8ffe4c0fanned root + invocation slot,dfc1df9routine root + source axis,23415d1discriminated arm + interface root,1c88a86lookup pin freeze,49de8cblookup fold closes the migration; In Progress → In Review9b292ea): 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 aLauncherCommandrow keyed by(coordinate, operation)and rendered byRootLauncherRenderer, a total function over the command taking no schema; the root fetchers reduced to thin entry points routing on row presence, andTypeFetcherGeneratorshed 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 (RootLauncherSqlBaselineTestplus 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: theextrasslot dissolved (cursor columns are a correlated view of the ordering slot), the spec’s payload-free-enum premise failed twice (ResultShapeandInvocationboth 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 (whereglue refs and facet fragment refs cross the seam as data). Validator-mirror gaps surfaced but not owned filed asroadmap/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 (
4d5f48dslice 1,7cac278slices 2+3, slice 4 with R541’sb7032ca`+395b4c4; Spec revisions `4d6e15a`+d1e22d8, Ready `61fde1f, In Progress1ed6f68, In Reviewcd949ec): 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 (Generatedterms rendered in the glue body,Authoredpredicates as calls into developer code, reach as producer-provenFkHoplists replacing four render-time throws), its total glueUnitMethodRef, shared-outer lifts and facet fragments as data with the fragment partition and local-name collision-freedom as compact-constructor failures.render/ConditionGlueRendererreplacedQueryConditionsGeneratorwith 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$fieldsarms, polymorphic branch folds, the facet-plan block) now emits the one-lineConditionGlueCall, andFkTargetConditionEmitterdeleted in full, so no host can compose a fold inline again ; the structural convergence enforcer. The entity layer (TypeConditionsGenerator, the<ReturnType>Conditions/<Participant>Conditionsclasses, theWhereFilternaming facts) retired with zero callers; slices 2 and 3 merged deliberately so the dual-scheme window and its merge machinery never got built, withEmissionLog’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 itsgraphitronContext(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 intocommand(ArgBinding,OuterLift,UnitMethodRefare 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 `ConditionSqlBaselineTestwhole-statement pins byte-identical, the one sanctioned delta (runtime-prefixed EXISTS aliases) decided before any pin was authored;ConditionMembershipTestpins relation key-set equals an independently derived covered set. Launcher handshake:LauncherCommand.whereis aGlueCallcopied 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 leafinstanceof104→97,case89→87. Independent-session In Review → Done review (this entry’s landing commit): full reactor green under-Plocal-db, retirement sweep swept one survivingcomputeLiftedOutersjavadoc cite and the closed window’s prose offEmissionLog, 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 scansConditionGluePipelineTestcarried through its rename. -
R551 (
16e45d1, in-review amendment11947dd; Backlog → Specda4b5a4, Spec → Ready47f3e3c, Ready → In Progressc7dd7ae, shipped-at notese12d2e0`+b88e509`+4077461, In Progress → In Review39ba5e9): Restated bothsrphand-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-dbas 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 namesroadmap/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. ATemplate design intentsection 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 siblingreviewer-promptskill 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 scoping51dfb78; Speca32d87d`+be8484f, Ready `60e6dc5, In Reviewa74895c): Collapsed the manual’s two hand-maintained directive indexes into one and made the survivor build-enforced.reference/index.adocreproduced the directives landing’s alphabetical and by-category columns inline and had drifted to 24 of 29 entries, missing@asFacet,@pivot,@routine,@scalarTypeand@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@pivotwas 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.DirectiveDocCoverageTestgrew 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:@referenceForwas filed under no category and@recordhad lost itsMappingslot when@scalarTypewas 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 shapeMethodCommandshould 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, thatCompileDependencyGraphBuilder.fromModelis 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 ofroadmap/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; filed646e779, duplicate-id collision resolvedd77c03c, Backlog → Spec7fd05fe, Spec → Ready5e32f47, In Progress → In Review85d2f52):CLAUDE.mdis 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-examplecompiles emitted sources at 17 to verify the generator’s output ceiling, whilegraphitron-jakarta-restpins itsdefault-compileexecution 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 indevelopment-principles.adoc. The module enumeration named eleven of twelve, omittingdocs, while the same file’s Documentation site section correctly describeddocsas a Maven module;modules.adoccarried the identical off-by-one in prose and omitted thedocsrow. The test-tier pointer aimed atdevelopment-principles.adoc, whose own text defers tier names and the decision rubric tohow-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-Pquickare named, with the-am/-amdprecondition moved into the surrounding prose so it stops contradicting the bare--plrule three lines above. Thepublishskill 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. Tworoadmap-tooltripwires bound toverifyin theAdocMarkdownTableCheckshape close the drifts for good.TransientCitationCheckfails the build on an item id or non-permanentroadmap/path in a declared prose document, which is where the rule thatRoadmapReferenceGuardTestenforces 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.ModuleEnumerationCheckrequires 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 emittedsystemMessage, which is user-facing, so a nudge aimed at the session that had just pushed landed on the human to relay; it now emitshookSpecificOutput.additionalContext. Moving the payload exposed that the declarativeifscoping was never filtering (asystemMessagenobody 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 offjqmissed a push on the second line of a compound command. Deliberately left open and filed as R547: the same citations live in ninepom.xmlcomment blocks and in the publisheddocs/architecture/pages, including two in themodules.adocrow this item edited. Coverage: 24 unit tests across the two checks, each asserting this repository clean from the test tier as well as atverify. 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-commandpayload; full reactor green under-Plocal-db. -
Discarded: Finish retiring pure-verdict GraphitronSchemaBuilderTest rows onto the classified corpus (
classified-corpus-verdict-row-retirement, R539) (726cc71; filed632c676, prose corrections60b2933): 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.mdhas all 35 whitelisted rows ticked plus a Slice 3 closeout section recording the endpoint (union net retired, output-field and type coverage moved toClassifiedCorpusas single source of truth, long tail swept in), andVariantCoverageTest.everyOutputFieldAndTypeLeafIsDemonstratedByTheCorpusconfirms it in code by readingcoveredLeaves()alone. A re-derivation over the live enum found zero pure-verdict rows left: of 400ClassificationCaseconstants the buckets are rejection/warning, slot-asserting, and input-side, all excluded from retirement by design, and the five apparent pure hits all readreturnType().wrapper()asserted withisInstanceOf, which is a slot read, not a verdict. The misleading signal was a large survivingGraphitronSchemaBuilderTest(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 in60b2933(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-corpusskill 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 trustingVariantCoverageTest(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 (
4ae2ce6core, reviewer-pass javadoc altitude fix in the approval commit; filed6c89e93, Backlog → Specbb6c646, Spec → Ready2218558, Ready → In Progress665d9ec, In Progress → In Reviewc22bb0d): 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.JooqTableRecordTypenow names bothfqClassNamepopulations (reflected table-record class vs the stand-in mints that assert only a projected-row source) and points atPojoResultType’s permit-identity split as the intended carrier, without blessing the null; `ReturnTypeRef.ResultReturnTypestates the copy contract (null exactly when the source component is) andTypeClassification.JooqTableRecord’s third restatement was aligned in the same edit. The garbled `InputTypesentence is reconstructed against the code, not the spec’s draft: an input carrying@tablealways classifies asTableInputTypebecauseTypeBuilder.buildInputTypeshort-circuits before the binding resolver runs, so the@tabledisjunct 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, withTypeClassification.PojoInputandPojoInputType’s own false "not specified in the directive" attribution corrected alongside. The `ErrorChannelemit partition became compile-checked: a new sealedErrorChannel.RouterDispatched(permitsPayloadClass,LocalContext) partitions the hierarchy againstMapped,WithErrorChannel.errorChannel()widened toOptional<? extends ErrorChannel>so each field variant declares its narrowest partition as its own record component, and thecatchArm/asyncRouterCallseams plusChannelCatchArmEmitter.emittook the narrowed types ; retiring all three "Additive window" runtime throws, the emitter’s unreachableLocalContextarm, and its sentinel parameter. Reachability analysis first established there was no live bug (Mappedis minted only byFieldBuilder.buildServiceFieldand routes exclusively through the wrap fork), so the narrowing pins a property rather than fixing a defect.RecordBindingResolver#fromAnyProducer, itsreachableset, and the set’s population are deleted; the one live reachability read already derives the fact at the read site. Five deadbuildSplitQueryDataFetcher/buildRecordBasedDataFetchercites resolved three ways, withGraphitronSchemaValidator’s guard rationale truth-verified arm by arm against `buildBatchedDataFetcherrather than renamed, and one rationale made honest:BatchedLookupTableFieldnow 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-slotOn.ColumnPairsstate 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 (@tableMethodsub-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.Mappedslots 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; Specb381122, survey fix7ed10f9, Ready160bec6, In Progress4665877, In Reviewf14ccc7):graphitron-sakila-example/README.mdapp section reconciled with the R399 jakarta-rest extraction. The "three files cover the runtime" framing and the deadGraphqlEngine/GraphqlResource/AppContextlinks 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 tographitron-jakarta-rest; thenewExecutionInput()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 underapp/withGraphQLOverHttpConformanceTestas the canonical exemplar,querydb/in-process) instead of the rotted census, and the pom-shape sentence gainsgraphitron-jakarta-rest+ Yasson. New enforcerReadmeLinkIntegrityTest(@UnitTiermarkdown twin ofManualXrefIntegrityTest) fails the build on any dangling relative link in a reader-facingREADME.md, verified to bite on the pre-fix README’s three deadapp/links;modules.adocnow names this README as the one deliberately-maintained per-module README, with the guard as its enforcer. Ride-along: dropped the doubledgraphitron-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 theGraphitronApplicationSPI 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; Specf5de169, In Progress38f38ad, In Reviewd856b77): 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; `outputTargetsrenamedfieldTargets), survivor directive definitions seed their argument types (sofederation__FieldSeton@keyclassifies),ClassifyingVisitorgained input/scalar/enum leaf arms, and the pre-walk leaf sweep inTypeBuilder.prepareForWalkwas deleted. Leaf classification is now reachability-pruned exactly like output composites: a declared-but-unreached input/enum/scalar is absent fromtypes(), including aSortDirectionretained by the all-declaredretainedSupportTypes()scan but reachable from no coordinate. The spec’s enabling fact (registry-read-free field classification) held lexically forFieldBuilderonly; the transitive leaf reads were re-routed rather than worked around: keyed reads go through the newBuildContext.lookAheadVerdict(delegating to the memoizedTypeBuilder.lookAheadVerdict), the scalar axis became theBuildContext.scalarVerdictsfixed point built beside the node/table/error indices, andclassifyAndRegisternow registerslookAheadVerdict’s verdict so a reachable multi-producer-rejected input re-registers the exact seeded `UnclassifiedType(equals-idempotent, typedRecordBindingMultiProducerpayload preserved). Pinned bySchemaReachabilityTest.walkClassifiesLeavesAndPrunesUnreachedOnes,SingleWalkClassificationOrderTest.noInputTypeIsRegisteredBeforeItsDiscoveringFieldIsVisited, andR96RecordBindingPipelineTest.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, filedec86c95; collapsed Backlog → In Review flow):TenantFanOutExecutionTest.claimedButUnmappedTenant_failsTheRequestBeforeAnySqlfailed 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 substring99in about one draw out of ten. The predicate now matches the full redacted template (An error occurred\. Reference: [0-9a-f-]{36}\., the shapeGraphQLOverHttpConformanceTest.REFERENCE_MESSAGEalready 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 Review23d58e9; assessment1bae5fe, Spec6da07b0, Readycdab0c8, In Progress28e27e3): The hand-maintainedSchemaDirectiveRegistry.GENERATOR_ONLY_DIRECTIVESsurvivor-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 sakilaschema.graphqlsfiles and the programmaticGraphitronSchema/per-type classes), exposing build-time database internals such as routine names and column mappings to any introspecting consumer. The existing pipeline sweepSchemaSdlEmissionTest.emittedSdlCarriesNoGraphitronInternalSurfacewas blind to exactly this drift class because its oracle wasisSurvivoritself. Fix derives the set instead of extending it: newDeclaredDirectivesinrewrite.schemaexposes the directive-definition names parsed from the bundleddirectives.graphqls, sharing a single parse withDirectiveSupportTypes(same resource, same derive-not-hand-maintain precedent), andGENERATOR_ONLY_DIRECTIVESnow reads that set, so a directive is generator-only iff declared indirectives.graphqlsand the drift class is unrepresentable; the registry javadoc drops the severed "kept in sync withBuildContextDIR_*`" contract (the `DIR_*constants stay hand-maintained deliberately, as classifier lookup keys). The spot-check unit test became a 32-namecontainsExactlyInAnyOrderpinning test (mirroringDirectiveSupportTypesTest), 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 noroutine/asFacet/pivotdefinitions or applications (only survivorauth/oneOfapplications remain programmatically); full reactor green under-Plocal-db. Surfaced by the R483 javadoc drift audit. -
R97 (Phase 2
85d8be0, Phase 2b30fde4b; Spec8682ef0/47f6765, Ready0993d11, In Progress117bd9a, In Review4f7326a): Consumer-derived input-table resolution, the slice of the@table-on-input deprecation that shipped. Phase 2 made the lastTableInputType-gated call site (arg-level@lookupKey) consumer-derived: a plain (non-@table) input carrying@lookupKeyre-derives its fields against the consuming field’s return table throughTypeBuilder.resolveInputFieldsand builds the sameTableInputArgthe@tablebridge produced (FieldBuilder.classifyPlainLookupKeyArg), so the composite-key lookup shape (FilmActorKey) no longer requires@tableon 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.buildLookupBindingswidened from aTableInputTypeto a resolvedList<InputField>(both callers reach it fully resolved, so the SDL re-walk and its deadnullarm went). With that arm consumer-derived and the mutation verbs field-relative,TypeBuilder.buildInputTypeis now directive-driven only (explicit@table→ the deprecated bridge, everything else plain): thefindReturnTablesForInputauto-promotion aggregate and theisUsedWithOverrideConditionrouting gate are both deleted, so an input reused across tables resolves per-consumer instead of silently demoting. The LSP surfaces the inference:TypeClassification.PojoInputgainedresolvedTables, 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@tablebridge): INSERT and UPDATE now share one resolver (FieldBuilder.resolveReturnCapableWriteTarget), UPDATE joinedRETURN_DERIVED_TABLE_VERBSandTABLE_ARG_SUPPORTED_VERBS,rawArgUpdateRejectionand 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-@lookupKeyresolve/reject pipeline cases, per-consumerPlainFilterrewrites, 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 aupdateFilmReturnDerivedsakila execution round-trip with@tabledropped from the input; carrier equality, never code-string assertions. The remaining phases are their own items: R518 (argMappinggrouping 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 inroadmap/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 fixes639e277; Spec63f6efa, Ready0faa819, In Progress89b3d8f, In Review3f109f8): 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@tablereturn, or a carrier payload’s single@table-element data field, both already carried by the classified model (TableBoundReturnType.table()/ the structural payload scan’sDmlElementKind.Table); rung 2 is@mutation(table:)on the field, for the encoded-ID / scalar return whose return names no table (INSERT joins the single-sourcedTABLE_ARG_SUPPORTED_VERBS, so the unsupported-verb guard narrows to{UPDATE, UPSERT}and the change flows tomvn graphitron:validate, the LSP, and the binding grounder automatically); rung 3 is the deprecated input@tablebridge. 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-existingrequireDmlDataTableMatchesInputTablewording 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-portableMutationInputResolver.resolveDmlWriteTableRefgains the return-derived rung and both verb gates, so the classifier (FieldBuilder.resolveInsertWriteTarget, dispatched to from the newclassifyInsertTableField/classifyInsertPayloadFieldmirroring 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-retiredMutationInputResolver.resolveInput(its INSERT admission set ;admitMutationInputFields,rejectPlainColumnCollision,rejectInputFieldDirectives; hoisted to shared statics and run over one resolvedInputFieldlist regardless of path; the field-derived path re-derives fields throughTypeBuilder.resolveInputFieldsand mirrorsGraphitronSchemaValidator.collectInputFieldRejections); the UPSERT deferral moves to the dispatch. The R332@table-on-input deprecation warning is now actionable for INSERT: theencodedWriteTargetInputTypescarve-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@tablereturn) 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@tabledropped, and the deprecation-warning flips;mutation.adoc/table.adoc/deprecations.adoc/code-generation-triggers.adocupdated. 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 retiredresolveInput(a dangling{@link}in the grounder javadoc, plusEnumMappingResolver/DmlKindclaims 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-tenantOutcomescatter contract, executor/cap/timeout on theGraphitronRuntimeconstructor, theTenantConnectionsthread-safety rework with the straggler and re-entrancy invariants, and the considered-and-deferred full-async-fetchers record with its evidence trigger. -
R511 (
d9c4b95; Spec2e4fe78, Readyf44fab4, In Progress3d61fce, In Reviewf0516bd): A@splitQuery @servicechild whose key wrap isSourceKey.Wrap.TableRecord(the ergonomicSet<XRecord>service signature) rebuilt the parent’s DataLoader key by reading reserved__src_<col>__aliases offenv.getSource(). Those aliases exist only on an SQL-projected parent row (<Type>.$fieldsadds them under the reserved-alias scheme that fixed the multiset-alias collision); a@tabletype handed back raw by a@service(selectFrom(X)→Result<XRecord>, never through$fields) carries only the real columns, so every such child threwIllegalArgumentException: Field "__src_..." is not contained in row typeat runtime, defeating a combination the manual already documented as supported. Fix makesGeneratorUtils.buildKeyExtraction’s `TableRecordarm 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-nameinto(...)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 sameallColumns()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 ofenv.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, andParentProjectionContainmentCheckare byte-identical to pre-fix output. The discriminator’s basis (a$fieldsSELECT materialises a genericRecord, never the typed subclass) is pinned byServiceParentTableRecordKeyExtractionTest(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 spinefilmsByService { titleTitlecase }(service-returnedFilmRecordparent; 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 aServiceProjectionPipelineTestshape predicate asserting the generated fetcher body carries both read families (shape assertion, not a code-string body pin).handle-services.adoctightened 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 Reviewab17c29, Spec7658c81/994e7c3/6cf2c9b): Scrub the deleted classification vocabulary from prose across every surface. The retiredSourceKey.Readerfour-arm family (ColumnRead/SourceRowsCall/AccessorCall/ProducedRecordRead), theBatchKey.*permits (AccessorKeyedSingle/AccessorKeyedMany/LifterPathKeyed),SourceKey.Cardinality(bareCardinality.ONE/MANY), and theMapped{Row,Record,TableRecord}Keyedservice-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/SourceRowsCall→KeyLift.Lifter(LifterRef);ColumnRead→KeyLift.FkColumns();ProducedRecordRead→KeyLift.ProducedRecords(Arity);Cardinality.*→Arity.*;Mapped*Keyed→SourcesShape(SourceKey.Wrap.*, LoaderRegistration.Container). The mis-shapedSourceKey(Wrap.Row, parent.PK, ColumnRead, Cardinality.ONE)constructor prose (which named componentsSourceKeynever carried) is rewritten as aKeyLift.FkColumnslift with a residueSourceKeyover 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):AccessorDerivedBatchKeyTest→AccessorDerivedSourceTest,GraphQLQueryTest.inventoryById_filmCardData_firesAccessorKeyedSingleLift...→...firesAccessorLift...(with itscomputed-fields.adoccitation), fourServiceCatalogTestclassifiedAsMapped*Keyedmethods →classifiedAsWrap*,InterfaceFieldValidationTest.rejects_listArm_onAccessorKeyedMany...→...onAccessorMany..., and the SDL-cited fixture methodsTestFilmService.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.derivePolymorphicHubSourceproduces aKeyLift.Accessorkey for class-backed parents, consumed byMultiTablePolymorphicEmitter’s `KeyLift.Accessorbranch and pinned byKeyLift.checkResidueAgreement);classifyParticipantRoutedoes constructParticipantCorrelation.JoinedCorrelationfor multi-hop/condition routes (not a deferred rejection), emittable viabranchBridgingJoins/singleBranchCorrelationWhere;ChildField.Transporthas three live arms includingWrapperArm;TableTargetFieldpermits six variants, not eight; the error-channel carrier classifier shipped (FieldBuilder.resolveErrorChannel); a single-table discriminated@serviceinterface routes to the sibling{Mutation,Query}ServiceTableInterfaceFieldleaf 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, inmodel/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 (theParentCorrelationinvariant 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 theChildFieldleaf-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-dbincludingRoadmapReferenceGuardTestand the javadoc{@link}-reference gate. -
R500 (
6cc947f, In Review55818e7, Spec9d1a788): Aliased duplicate reference selections (a: ref { x } b: ref { y }) minted duplicate SQL aliases, because the$fieldsselection 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 twoDSL.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 singleTableField,LookupTableField,ComputedField, DirectColumnReferenceField) 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_foomints__rk___rk_foo, still distinct). The four families' reads move from source-onlyLightFetcherbindings to env-dependent bindings; scalarColumnField/CompositeColumnFieldarms stay as-is (alias-independent typed-constant reads). Membership single-homed as the recommended design fork: a methodless markerResultKeyAliasedFieldon the four families plus a build-time guard on each consuming side’s fall-through (TypeClassGenerator.emitSelectionSwitch’s `defaultarm andFetcherEmitter’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 itsparentProjectionAliascluster) 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 toReferenceProjectionAliasNamespaceTest; (b) the single-table@discriminatebase-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 unaliasedallSubjects/allPartiescase). Coverage: unit/pipeline signature assertions (env-dependent method references, no code-string body assertions),ReferenceProjectionAliasNamespaceTest, and five execution-tier enforcers inGraphQLQueryTest(mixed aliased+unaliased scalar/computed,LookupTableFielddivergent args + sub-selections, listTableFieldduplicate, 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@asConnectionquery (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 Progress93e5d16, Specf6d4d9e/1bb4ad6): A Relay connection’s<Node>.$fieldsprojection built the SELECT from only the first occurrence of each shared result key, so selecting the same reference field under bothedges { node { ... } }andnodes { ... }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 (silentnullon that side). graphql-java’sgetFieldsGroupedByResultKey()flattens the whole subtree, so the two paths collapse into oneList<SelectedField>bucket per key; the loop boundsf = entry.getValue().get(0)and recursed over only that occurrence’s selection set. Fix moves the union onto the selection side: a new frozen-scaffoldSelectionOccurrences(SelectionOccurrencesClassGenerator, registered inUtilSingleton.ALL) carriesmergeByResultKey(concatenate every occurrence’s sub-selection into one insertion-ordered grouped map, so each arm still emits exactly oneDSL.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$fieldsentries (DataFetchingFieldSelectionSetandList<SelectedField>) delegating to one private$fieldsGroupedswitch loop; the inlineTableField/LookupTableFieldarms descend via$fields(entry.getValue(), ...)andNestingFieldrecurses 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 aGraphitronClientException(surfaced raw pastErrorRouter, not redacted): the name guardcanonical(key, occurrences)is universal (runs per bucket before every dispatch, since occurrences disagreeing ongetName()are two fields aliased to one key), the argument guardrequireConsistentArgumentsis arm-scoped (emitted only where the arm consumessffor runtime state, driven by theFromSelectedFieldpredicateInlineTableFieldEmitter.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-tierConnectionSharedResultKeyProjectionTest(10 tests: all four divergence directions onStore.customers, one level deep, a polymorphicsearchConnection, argument divergence/agreement oncustomersFirstN, name divergence on aliasedsummary/infoNestingFields, 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 Review608b793, Spec → Readyc1c0e5f): Purge transient roadmap references (R<n>/roadmap/<slug>) from string literals across the in-scope modules, and drop theRejection.Deferred.planSlugfield that rendered one. The structural centerpiece:planSlugis removed from the model, thedeferred(...)factories collapse todeferred(summary)/deferred(summary, class), andDeferred.message()becomes a pure function ofsummary, so a deferred rejection can no longer compose asee roadmap/<slug>.mdsuffix onto the author-facing validator log or the runtime stub’sUnsupportedOperationExceptiontext. 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-unreachableTypeFetcherGeneratorstub) lose it cleanly, and theFieldBuilderDML-pair slug threading andTypeFetcherGenerator.deferredForslug 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 anR246 / R258 / R266citation), and documentation text emitted into generated output (habitat C); plus the two@reference-path channel-bypass deferrals and the three synthesis-shimLOG.warnsites. Enforcement is one guard over two lexical habitats:RoadmapReferenceScannergains 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), andRoadmapReferenceGuardTestadds 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@DisplayNameor 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.adocdrops the false LSP-fix-it claim, theplanSlugfield, and the non-existentStubKey.EmitBlockarm (from both the prose and the mermaid class diagram); four user-manual pages no longer promise a roadmap-path suffix. Test reworks assert onsummary/stubKeyrather than a slug, andRejectionRenderingTestpinsmessage()both equal tosummaryanddoesNotContain("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,graphitronmodule compiles all call sites); full reactor green under-Plocal-db. Builds on R482 (comment/javadoc guard); subsumes R491/R493. -
R488 (
ab58f63, In Review1432ff1, Spec → Ready3b6ba97): 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>...]", alongsidedata-concept-title: a comma-separatedR<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 tokenR[1-9][0-9]*, required and non-blank), an allocated-id bound (n < changelog next-id, so a typo likeR999fails 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 headerkickerline, which now readsConcept explainer · R<n> · theme: <theme>, so the visible restatement cannot silently drift from the attribute).ConceptPages.readTitles(slug → title) is replaced byreadPages(slug →ConceptPagecarrying title + backing ids), taking the roadmap dir so it can readchangelog.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 newConceptIndex: a declared id becomes aLive(id, slug)orShipped(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 themapHrefone-place discipline. Both directions derive from the declaration: item side, anexplainerlink 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 withdata-concept-items="R458"and the kicker update, proving theShippedarm live (below next-id, no listing link, plain-text annotation); theLivearm is covered by tests. Theexplainerskill 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:ConceptPagesTestextended to 35 (items parsing incl. whitespace/missing/blank/malformed; allocated-id bound incl. then == next-idboundary and the shipped-but-below case; kicker enforcer;readPagesshape and absent-directory empty;ConceptIndexLive/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;stageenforces 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,ConceptPagesTest35/35) and README regenerates with no drift. -
R483 (per-slice
2f2dcb8/a40a2e4/da2a248/5fe681d/e7a4a25/434dc90/44a9931/557f332/2f55024/19f4481, outcome + follow-ons2d4b6c2, In Reviewd4ff640): 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-lineFieldBuilderwas drift-marker-grepped rather than full-read), landing 89 net edits (90 applied, 1 reverted by the adversarial verify stage, which restored a liveGeneratedSourcesLintTest.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’scheck-link-referencesgate), 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.SplitLookupTableFieldrepointed to the liveBatchedLookupTableField, the retiredBatchKeyclassification vocabulary corrected to the currentKeyLift.*/SourceKey.Wrap.*names, and the stale legacy-GraphQLGeneratorclause deleted. Six Backlog follow-ons were filed for still-unpinned load-bearing claims: R493 (roadmap ids in generated-output javadoc), R494 (SchemaDirectiveRegistry.GENERATOR_ONLY_DIRECTIVESout of sync withBuildContextDIR_ROUTINE/DIR_AS_FACET, a possible correctness bug), R495 (InputRecordGeneratorservice-audit javadoc contradiction), R496 (MappingsConstantNameDeduptwo-vs-threeErrorChannelarms), R497 (FederationSpec.URLcaller 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-restandgraphitron-sakila-exampleno-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 undermvn install -Plocal-dbwith the R492 reference gate andRoadmapReferenceGuardTestactive. Sequenced after R482 (de-noised surface); depends on R492 for relink to count as a real pin; spawns R493-R498. -
R492 (
adfcb5a, In Reviewe0baef1; Specc5d284e/15ce2a0, Ready78b9933): Add a{@link}/{@see}-reference-validity gate to the routine build, so a dangling doc link failsverifyinstead 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 themaven-javadoc-pluginjavadocgoal (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 themissing-@param firehose) toverifyin the main<build>— deliberately not anactiveByDefaultprofile, which Maven silently deactivates whenever-Plocal-dbis named; the fast-local escape reuses the existingmaven.javadoc.skiplever that-Pquickalready sets, and CI’s singlemvn verify -Plocal-dbpicks it up for free. Turning it green meant driving the 41 pre-existing broken references to zero (measuredgraphitron30,graphitron-sakila-service10,graphitron-lsp1): resolvable links were qualified or FQN-labelled and kept as checked{@link}(nested variants, cross-package public symbols, two cheap repointsOutputField#source(Arrival)andTable#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 (BatchKeyarms,ChildField.SplitLookupTableField, the legacyGraphQLGeneratorclause) was flagged into R483’s seeded-findings rather than guessed at. Anti-vacuous-pass guard:JavadocReferenceGateTestpins that the gate is bound in the main build (not a profile), that no covered module opts out withmaven.javadoc.skip, and that covered modules have hand-authored sources; the sourceless/generated-only modules are structurally exempt.development-principles.adochad its now-false "`{@link}: no audit infrastructure" grade rewritten to name the gate (still under the `DocSizeBudgetTestword budget) and CLAUDE.md documents the-Pquickskip. 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 undermvn install -Plocal-db(unpiped exit 0; one transientBgeEmbedderOnnxTestsimilarity flake under parallel load, unrelated, passed on rerun). Prerequisite for R483’s relink tool to count as a real pin; sibling to R482. -
R314 (
d1f13a2site-level fact + command registry + bidirectional oracle,7137d1e/4abde9eslices 2a/2b,4e04345slice 3,11122a4slice 4,1158c14slice 5,88ddf90docs sweep + In Review; plan6590535, 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 factOutputField.emitsKeyedReQuery()next to the value-levelrequiresReFetch()(false exactly on the root@servicepassthrough, whose re-projection is realized by downstream$fields), the main-sourcemethodgraphcommand/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 onGenerationResult.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 mergedbuildSplitQueryDataFetcher+buildRecordBasedDataFetcherinto the one source-shape-gatedbuildBatchedDataFetcher(the storedSourceShapegates exactly the two facts it owns, key lift and Outcome/LocalContext prelude; byte-identical sakila output). Slice 2b landedTableExpr.MethodCallas its javadoc reserved and dissolvedRecordTableMethodFieldonto the record-sourcedBatchedTableFieldarm (the developer’s method is the terminal hop’s materialization fact rendered by the shared table-expression switch;SqlRecordTableMethodpermit 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 (validateServiceTableFieldrejects 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 namedrows<Name>companions (model-carriedDmlTableField.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’sdispatchPerformsReFetchmirror and landed the replacement reentry implementedness guard (site-level fact on a leaf outsideBatchKeyField/DmlTableField→ ValidateMojo error) in the same commit, no window. Acceptance held: R305 reentry execution tier green (SingleRecordPayloadDmlTest,SingleRecordTableFieldServiceProducerExecutionTest),@classifiedcorpus 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 insplit-vs-inline.adocper the workflow’s user-facing-doc check, the registry’s stale pre-slice-4 DML paragraph, a malformed javadoc indent inChildField) and verified the full reactor green undermvn install -Plocal-dbafter remediating two sandbox artifacts unrelated to the diff (stale DB seed predating R458’scategory_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 filedda0cc2c): 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(agraphitronunit-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), andRoadmapReferenceScannerTest(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 undermvn install -Plocal-db(guard testsRoadmapReferenceGuardTest1/1,RoadmapReferenceScannerTest11/11). Spawns R490/R491; separated from R483/R484. -
R481 (
f5b2eb5/f1515fa, In Review3a67937, Spec → Ready0be64fa): 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/UnionFieldfell into thedefault -> {}arm ofTypeClassGenerator.collectRequiredProjection, so a parent-holds-FK participant (self-FK navigate-to-parent, or a cross-table FK likestore.address_id → address) whose parent-side correlation column no client selection projected crashed withIllegalArgumentException: Field "<fk-col>" is not contained in row typeat 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 orthogonalParentRowDemandcapability (a standaloneList<ColumnRef> parentRowColumns()interface, mirroringBatchKeyField) carrying the whole projection demand, implemented byTableMethodField(its single-FK-hop source-side columns; the existing dedicated arm collapses onto the capability) and by the polymorphicInterfaceField/UnionField, cardinality-forked: single returns the union acrossparticipantJoinPathsof whatMultiTablePolymorphicEmitter.singleBranchCorrelationWherereads (KeyTupleWhereslot /JoinedCorrelationFK hop-0 slotsourceSide()columns; the parent bound key for a condition hop-0;On.Lateralhop-0 throws), list/connection returnsparentSourceKey.columns()(the batched key extraction). Three walks consume it keyed on the capability, never on leaf identity:collectRequiredProjection(capability arm + record-source tripwire extended toParentRowDemand),ParentProjectionContainmentCheck(capability-keyed requirement enumeration alongside theBatchKeyFieldone),GraphitronSchemaValidator.collectBaseNamedKeyColumns(capability arm replaces theTableMethodFieldcase). Gap C (batched parent-holds-FK correlation, a genuinely absent capability) is closed at build time only:FieldBuilder.classifyParticipantRoutethreads 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 newparentReffixture inMultiTablePolymorphicSelfFkOrientationExecutionTest(projection-independence proofs that select no parent-key-mapped field; the cross-table twin usesStore.contactover address/staff participants, whose nullablestore.manager_staff_idgives 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 inpolymorphic-types.adoc. Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db. Builds on R458; spawns R487 (batched-polymorphic-parent-holds-fk-correlation). -
R458 (slice 1
cbb6eb1, slices 2-35968d53, rework4bf1d0c, In Review7fade80, Spec → Ready482c693): 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!]!)onFIELD_DEFINITIONstates one participant’s join path from the parent table, overriding single-FK auto-discovery for the named participant only (participants left unnamed keep discovery);pathreuses the@referenceReferenceElementgrammar unchanged. The syntax consult rejected reviving@multitableReference(itsroutes: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@referenceForapplications are keyed bytype:and independent, the opposite of@reference’s concatenating chain, a contrast the reference page carries). Model: a two-arm sealed `ParticipantCorrelationonChildField.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), andJoinedCorrelation(List<JoinStep>)(the branch joins real tables) for multi-hop FK chains and any route carrying anOn.Predicatehop;participantJoinPathsbecomesMap<String, ParticipantCorrelation>dispatched exhaustively by all three cardinality emitters (singlebuildStage1Block, batched list, batched connection). Sliced: slice 1 shipped the directive, classification, carrier generalization, and the multi-FK / same-table-self-FK cases (both lower toKeyTupleWhere), 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 viaJoinPathEmitter.emitBackwardBridging, value-bind hop-0 againstparentRecord/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, intermediatefilter:hops ANDed onto the branch WHERE). Slice-1 review fixed an orientation bug (every@referenceForroute was resolved with a hardcodedisList=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-0filter:(a{key:, condition:}heading the path) was dropped, not rejected, in both arms (KeyTupleWherecarries only the FK column pairs;JoinedCorrelation’s `appendHopFilters/hopFilterTermsreads only intermediate hopsi>=1), soFieldBuilder.classifyParticipantRoutenow 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: newreferenceFor.adocreference 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-tierMultiTablePolymorphicJoinedCorrelationExecutionTest(multi-hopfilm -> film_actor -> actor, condition via anEXISTS-over-film_actor, auto-discoveredinventorysibling keeping the field multi-table) andMultiTablePolymorphicSelfFkOrientationExecutionTest(list-direction self-FK on acategoryhierarchy). Independent-session review across slices; In Review → Ready → In Progress → In Review → Done. Full reactor green undermvn 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-participantfieldsJoin/orderByemission (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 Review82846ff, Spec → Readyd1a8ba1): A roadmap-adjacent developer explanation space atroadmap/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 inMain.mapAdocTargetis extracted into a sealedLinkTargettaxonomy (plus newConceptPageandDeepDocsPathcases), the AsciiDoc emitter is refactored onto it behavior-identically, and a new HTML href emitter inConceptPagesformats 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:8aab6a6captured characterization assertions for every pre-refactormapAdocTargetbranch (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.ConceptPagesalso owns thedata-concept-titlecontract (missing or blank fails generate, verify, and render-adoc alike, naming the file), therender-adocstaging 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, soverifycatches listing drift).docs/pom.xmlgains a<resources>entry carrying the staged pages intogenerated-docs. The R458 sketch page is refit onto sharedconcepts.css/concepts.jswith the shared header, title attribute, derived-region fences (derived:status/derived:backlinks, owned by the skill’s refresh mode), and an em-dash sweep. Theexplainerskill 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 ingenerated-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 Review75f0b19, Spec → Readydf29e28): 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+RecordTableFieldmerge intoBatchedTableFieldandSplitLookupTableField+RecordLookupTableFieldintoBatchedLookupTableField, each gated on a storedSourceShape(Table|Record) and spending R431’s types: the lift becomes total, the split arm now carryingKeyLift.FkColumnsalongside itsSourceKey(entryColumns, Wrap.Row)(the census established the stored difference was exactly oneKeyLift liftcomponent and the derived-method fork wasemitsSingleRecordPerKey’s `|| LOAD_MANYdisjunct, unreachable on the split arm). The gate is stored, not derived, sinceFkColumnsis 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-gatedsourceShape==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, andTable ⟹ LOAD_ONE(deriveSplitQuerySource’s former prose guarantee made structural, which is what keeps the unifiedemitsSingleRecordPerKeyformula 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 `BatchKeyFieldarm,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:ParentProjectionContainmentCheckcross-checks the auditedcollectRequiredProjectionwalk against an independent demand enumeration at the$fieldsemit site, keyed onBatchKeyField+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 throwsIllegalStateExceptionas a generator invariant (documented at the throw site as deliberately not a typedRejection, 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,RecordLookupTableFieldstill absent from the LocalContext set). The fetcher fork stays behind onesourceShapeseam (Table viabuildKeyExtraction, Record viabuildRecordParentKeyExtractionwith 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 wholeKeyLiftaxis indispatch-axes.adocper 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 totalliftremoves an absence case and tells no lie" and R314 provisioning as the bonus), pluscode-generation-triggers.adoc, theKeyLift/FkColumns/BatchKeyFieldjavadoc, regeneratedsupported-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-mergedBatchedTableFieldValidationTest/BatchedLookupTableFieldValidationTest, renamedBatchedTableFieldPipelineTest,ReFetchDerivationTest,StubbedVariantPipelineTest, and the@classifiedcorpus (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 -rempty) 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 undermvn install -Plocal-dbfor the R432-bearing tiers (allgraphitronunit/pipeline/execution/audit tests pass), with the sole failure the unrelatedBgeEmbedderOnnxTestONNX-native-binding infrastructure test ingraphitron-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 Reviewbdb024c): Decompose the conflated six-componentSourceKey(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 denormalizedtargetcopy, its four readers migrating to a caller-suppliedkeyOwnerTable/ the leaf’s ownreturnType.table(), with the null-target case landing as a typedReturnTypeRefarm rather than a re-invented null; slice 2 (9c8261b) deletedpathand retired the transitionalJoinStep.LiftedHoponto a hop-lessParentCorrelation.OnLiftedSlots(targetTable, columns)arm (the four defensive unreachable emitter arms became type-level impossibilities, theHasSlotscapability died intoOn.ColumnPairs, and the two empty-path compact-ctor invariants are now pinned structurally); slice 3.1 (3229cbe) moved the arrival-vocabulary half ofCardinalityto a top-levelArityenum on the producer/carrier endpoint facts (deliberately not R463’s accumulatedArrival); slice 3.2 (2ae8529) shrankSourceKeyto the(columns, wrap)residue (which earns the name), relocated the four live reader arms to the new sealedKeyLiftfact (FkColumns/Lifter/Accessor(ref, Arity)/ProducedRecords(Arity)) carried by the three record-parent leaves and, asparentKeyLift, byInterfaceField/UnionField(the requiredparentSourceKeyre-typing), dissolved the service arms into theMethodRef.Param.Sourcedsignature andResultRowWalkintoSingleRecordIdFieldplus the first-classSourceEnvelopeenum, and deleted theReaderseal; slice 4 (6c610e7) consolidated the repeated bridging-join emit switch intoJoinPathEmitter(R438 cleanup 2). Wrap is stored where authored (split/service) and derived viaKeyLift.wrap()where inferred, withKeyLift.checkResidueAgreementas the constructor tripwire; the "hard"ResultRowWalk(OUTCOME_SUCCESS) => Wrap.TableRecordinvariant is re-asserted (strictly stronger, unconditionally) at its named join site,SingleRecordIdField’s compact constructor. Docs: the `dispatch-axes.adocchapter is rewritten onto the residue/lift/envelope/arity model with inbound xrefs retargeted. Coverage: unit-tierKeyLiftTestandSingleRecordIdFieldKeyShapeInvariantTestpin the migrated invariants,SourceKeyTestmigrated 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 undermvn 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 sealedInputLikeTypeparent, 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):@tableMethodis deprecated and withheld from the v1 surface (R400);@routinecovers 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@tableMethodsupport 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@tableMethodinlining folds into the future redesign. -
R202 (
2d2ab74, In Review02821dd, Spec → Ready73e805d): Honor@field(name:)on an@errortype’s extra fields (everything beyondpath/message) at both the classify-time source-accessor check and the runtime property read. The accessor-coverage check calledClassAccessorResolver.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()undercode,SQLException.getSQLState()understate) 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.ErrorTypegains aList<FieldAccessorOverride>component (FieldAccessorOverride(sdlFieldName, accessorBase), in SDL declaration order,List.copyOf-guarded, entries only for extra fields carrying the directive) populated byTypeBuilder.buildErrorType, plus a defaultingaccessorBaseFor(sdlFieldName)that returns the override or the field name itself. Both check sites,FieldBuilder.checkErrorTypeSourceAccessors(class-backed payload / DML path viaresolveErrorChannel) and R244’swalker.internal.HandlerAccessorCheck(@serviceoutcome path), passaccessorBaseFor(...)instead of the raw name, so the remap rule lives in the model and the two bodies cannot drift; each reject string (andErrorChannelWalkerError.HandlerSourceAccessorMissing, which gains anaccessorBaseNamecomponent) appends a(remapped to '<base>' by @field)parenthetical so a failed override is diagnosable. Runtime wiring:GraphitronSchemaClassGenerator.buildErrorTypeFieldFetchersnow takes theErrorTypeand, after thepath/messageregistrations, emitscodeRegistry.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 defaultPropertyDataFetcherby SDL name.buildErrorTyperejects a present-but-blank@field(name: "")on an extra field and any@fieldonpath/message(both synthesised by Graphitron, so the directive could never take effect). Stale javadocs onbuildErrorTypeFieldFetchersandErrorTypeFetcherClassGeneratorclaiming@errortypes are restricted topath+messageare corrected. Docs:error.adocgains an "Extra fields" section,field.adoccross-links it, and the@fielddocstring names the@errorextra-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 (@serviceviagetLocalizedMessage(), 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-namedgetAttemptedId()throughFilmLookupInvalid.attempted(-7), the first execution coverage of@errorextra fields. Independent-session In Review → Done review; full reactor green under-Plocal-db(all 13 modules, 593graphitron-sakila-exampleexecution 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 Review56f009b, Spec → Ready76e1136): Stop a malformed connectionafter/beforecursor 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 emittedConnectionHelper.decodeCursorturned client-supplied wire input intoField<?>[]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 siblingpageRequest(R415) and the single-record node-id decode. Fix wrapsdecodeCursor’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 customConverter) 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:encodeCursoremits 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 deprecatedDataType.convert(String)is lenient and returnsnullfor an uncoercible token rather than throwingDataTypeException(verified across numeric, temporal, UUID, and theMpaaRatingenum 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 tonull(a genuine SQLNULLis the\u0001sentinel, handled in the branch above), with theDataTypeExceptionclause retained as documented-defensive. No router or fetcher-emitter changes:pageRequestisdecodeCursor’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 `GraphQLQueryTestnext to the R415 guards, each pinning the exact message anddoesNotContain("An error occurred. Reference:"): bad Base64 (after), non-coercible token through thenull-guard on numericFILM_ID, under-split on the two-columnfilmsByRateDescTitleAsc, over-split strictness (new behaviour), and thebeforevariant; 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 undermvn install -Plocal-db(all 13 modules, 593graphitron-sakila-exampleexecution 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’stotalCountredaction bypass, and a configurable max page size. -
R478 (
d19a4a6, Spec289d7ce): SealKeyAlternativefrom an enum-carrying record into a sealed interface withDirect/NodeIdvariants, each carrying itsrequiredFields/columnscontract as structure rather than the unenforced javadoc prose R477 lived in.DirectstoresList<RepBinding>(rep field, column) pairs, so "sizes equal, index-by-index" becomes unrepresentable rather than a promise;requiredFields()/columns()unzip the pairs in order.NodeIdcarriesexpectedTypeId(the resolved wire prefix) pluscolumns(the decode arity), withrequiredFields()a derived constant["id"].HandleMethodBody.emitDecodeAndGroupbecomes 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 deadtypeNamefallback deletes: the decision moves toEntityResolutionBuilder, which buildsNodeId(nt.typeId(), ...)directly. Single-sources the typeId fact by droppingEntityResolution.nodeTypeId;EntityFetcherDispatchClassGenerator.buildTypenameForTypeIdMethodderives its reverse map from each entity’sNodeIdalternative (presence sets coincide with the formernodeTypeId != nulltest, inclusion ignoringresolvable, key isexpectedTypeId) and sheds its now-unusedKeyShapeimport. Also corrects the staleGraphitronType.NodeType.typeIdjavadoc (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 isKeyAlternative, its consumers/builder,EntityResolution,GraphitronType, and two test files (EntityResolutionBuilderTestrewritten toinstanceof+expectedTypeId/bindingspattern assertions plus aNodeId.requiredFields()==["id"]pin; aRejectNonIdNodeIdPipelineTestcomment). 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, 588graphitron-sakila-exampleexecution tests). Out of scope and untouched: any behavioural/emitted-code change, routing the batch decode through the per-typedecode<TypeName>helpers, and new key shapes (this item only prices their arrival as a compile error at every fork site). -
R474 (
2efbc47, Spec5d245ef/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.shfetches mvnd fromdownloads.apache.orgto/opt/mvndand symlinks/usr/local/bin/mvnd, failing open to plainmvnon any error (simulated download failure leaves no/opt/mvndresidue 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 fromgrep -qw mvntogrep -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 tomvn 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.mdgains prefer-mvnd-in-web-sessions guidance,.claude/web-environment.mdgains the mvnd section with the accepted quirks (Maven 3.9.16 skew vs/opt/maven3.9.11,-qstdout 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 undermvnd verify -Plocal-db -T 1Cin-sandbox (all 13 modules, 588graphitron-sakila-exampleexecution 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+ rework2c8aac4, review32169ba): Faceted search on@asConnectionvia a new@asFacetdirective on filter-input fields. Each marked field becomes an entry on a synthesised<ConnName>Facetsobject attached as a nullablefacetsfield on the generated Connection type, with per-(scalar, nullability)<Scalar>FacetValuevalue types (FacetNamingkeys the derived name on both so nullable and non-null facets over one scalar never collide;valuemirrors the filter element’s type exactly so facet values round-trip into the filter with no coercion). Synthesis ridesConnectionPromoter’s field-first walk; the resolved `List<FacetSpec>lands onGraphitronType.ConnectionTypeas a denormalized view carrier (contained route:Operation.Facetstays unpopulated behind the ConnectionType quarantine for R314 to fold in, matching thetotalCountprecedent), and newFacetsType/FacetValueTypesealed arms ride the per-type emit with all exhaustive switches updated. At runtime the root Query connection fetcher assembles a facet plan onConnectionResultfrom generated filter-minus-self fragments (<field>FacetBaseConditionover non-facet fields via null-literal suppression keyed on (argument, field) extraction identity,<field>Facet_<g>Conditionper facet), andConnectionHelper.facets(env)issues oneUNION ALLof per-facetGROUP BYarms (each under base AND every other facet’s predicate,IS NOT NULLscrub on non-null facets, TEXT-unified value column decoded back through the column’sDataType), lazy on selection, sorted post-decode on count DESC then the typed value’s natural order. The facets delegate routes failures throughErrorRouter.surfaceClientErrorOrRedactso a facet SQL failure degrades tofacets: nullplus a redacted error while the page resolves.rejectFacetMisuse(definition-keyed half extracted toFacetFieldValidation, one predicate shared with the promoter so the passes cannot drift) rejects every v1-unsupported shape at build time: non-@fieldbindings,@reference/@condition/@nodeIdco-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 liftedfilterMaplocal 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, replacedFacetEmitterTest’s banned code-string assertions with method-surface checks, and pinned the failure/redaction contract end to end. Full reactor green under `-Plocal-dbon the approval pass (one unrelatedBgeEmbedderOnnxTestflake, passing in isolation and on resume). Spawned R475 (duplicate Java parameter names when two filter args share a field name, pre-existing) and R476 (totalCountshares 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 Review0b9308f, Specfe68a59): Unify the four divergent SDL-field-to-Java-accessor resolution implementations behind one candidate model.ClassAccessorResolver.enumeratebecomes the single home for the name rules, the boolean-onlyis-gate (on the member’s own return type), the member filter (public, non-static, non-bridge, non-synthetic, notObject-declared), and the per-kind parameter-shape rule; it is parametric in theCandidateKindset a consumer accepts so each reduction’s switch is exhaustive over only the arms it can receive. Two reductions consume it:resolvekeeps its signature and first-match reduction (now picking up the bridge/synthetic/Objectfilter it previously lacked, a latent-bug fix, B6) andFieldBuilder.collectAccessorMatcheskeeps its table-identity/cardinality ambiguity reduction (zero-arg kind only), with the@sourceRow-hint ambiguity rejection preserved. A new discovery-directionprobe(sealedAccessorProbeGrounded | NoMatch, never a nullableType) replacesRecordBindingResolver.findAccessorReturnTypeandinferAccessorName, which are deleted: the result/input propagation and the R329producerBindLevelpresence 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).forBackingClasssingle-sources the walk’s class-shape→candidate-order rule, pinned equal to the emission-side variant derivation by an order-bridge meta-test overTypeBuilder.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-tierClassAccessorResolverTest(order/is-gate/filter/arity/field-fallback table, covariant-bridge skip, order-bridge meta-test) and pipeline-tierR461WalkGroundingPipelineTest(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’sCatalogBuilder.beanAccessorSlotthrough 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 populateSource.OnlyChildwhere the product isOne, retiring the conservative hard-codedSource.Childon everyChildField. NewArrival(latticeOne < Many,Manyabsorbing,tensorproduct) andArrivalIndex, a pure typename-keyed fold over the assembled (pre-connection-promotion) SDL: a composite type isManyif it carries a@node/@keyseed 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 inGraphitronSchemaBuilderand threaded ontoGraphitronSchema(parse-boundary containment: no graphql-java type survives into the storedMap<String, Arrival>);OutputField.source()becomes the storage-free derived viewsource(Arrival)(root leaves ignore the argument), read through the singleGraphitronSchema.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 ownsourceShapedirectly (arrival-agnostic, behaviour-identical), and theGraphitronSchemaValidatorre-fetch-mirror diagnostic readssourceOf. Model + tests only: emitters keep leaf-identity dispatch, so anOnlyChild-classified batch field still emits its one-element DataLoader and generated output is byte-unchanged; the direct-SQLOnlyChildemit and its enforcer ride the R431→R432→R314 chain (forward obligation recorded). TheSource.OnlyChildjavadoc carries the honesty clause (Oneis a static per-dispatch guarantee about unaliased projections; the arm must stay row-correct at every arrival count). Coverage:WrapperAlgebraTestpin lifted to the Root law + parent-grain law + a coverage floor observingOnlyChild/Child/Root;ClassifiedDslTest.SOURCE_KNOWN_GAPSloses itsOnlyChildexemption (now empty); corpus rows flip toOnlyChildwhere the fold yieldsOne, 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 inGraphitronMcpServeriscatalog.tables,catalog.describe,catalog.search,services,conditions,records,schema,diagnostics,edges,docs.search,execute, and thedirectivesresource. The one live open question (English-onlybge-small-en-v1.5-qvs 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 structuredservices/conditions/recordstools 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.BuildFailureexists and all four verify tripwires throw it (Main.runVerifyREADME drift,Main.validatefront-matter,LeafCoverageReport.rundrift/no-traces,AdocMarkdownTableCheck.runfindings), plus the later-addedDirectiveSupportReportguard; CLI/usage/create-file-exists paths correctly remain onSystem.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=trueis set in both the Surefire and Failsafe configurations of the parent pom, and thegraphitron-mcplogback-test.xmlWARN 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.htmlships a client-side mermaid.js module loader that rewritescode[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).resolveDecodeHelperForTablenow resolves through the@node-onlyNodeIndexand rejects multi-node ambiguity at validate time rather than silently pickingdecode<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: thefallbackTypeNameOrTypeIdargument 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/ConditionJoinreplaced byJoinStep.Hop/LiftedHop), and the stored alias is no longer dead:Hop.alias()is consumed extensively by the routine/chain join emitter inTypeFetcherGenerator, 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.generateAliasesstill derives its own per-hop aliases for the inline emitters instead of consumingHop.alias(), a dual derivation with no generated-code difference. -
R465 (
a9deae5, Specc3ae081/145afd1): Bump the two graphql-java satellite artifacts in the root pom dependency-management block,federation-graphql-java-support6.0.0 → 6.2.0 andgraphql-java-extended-scalars22.0 → 24.0, holdinggraphql-javaitself 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@scalarTyperesolver binds any public-staticGraphQLScalarTypeconstant 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 itsconventionTable_coversEveryExtendedScalarsFielddrift guard. Independent-session In Review → Done review; full reactor green under-Plocal-dbacross all 13 modules. -
R346 (
ab70c35/41c0b70): Regenerate and guard the generatedsupported-directives.adocmigration fragment against directive-set drift. The fragment (included bymigrating-from-legacy.adoc, emitted bydirective-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. Thedirective-supportCLI gains an additive--verifyflag mirroringleaf-coverage --verify: it regenerates the fragment in memory, compares against the committed--outputfile, and throwsBuildFailurewith a copy-pasteableREGENERATE_COMMANDon any drift (--verifyrequires--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 fromgraphitron-common/src/main/resources/directives.graphqlsat tag v9.3.0 (pre-R182 delete) intoroadmap-tool/src/main/resources/legacy-directives.graphqlsrather than read from the legacy module; a stripped-before-parse provenance header documents the source. A phase-boundverify-supported-directivesexecution inroadmap-tool/pom.xmlruns the guard on everymvn verify(PRs included), alongsideverify-roadmap-readmeandcheck-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 onrewriteByName.containsKeyminus the rejected/withheld sets) and states the real criterion (declared + supported, documented in the architecture chapter). Coverage:DirectiveSupportReportTestgains the prose assertion plus the--verifyround-trip (zero on match,BuildFailureon drift, exit 64 without--output); the guard is its own regression net. The siblingsupported-schema-shapes.adocfragment (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, Specf79855e/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:ScalarTypeResolverlosesCONVENTION_TABLE,conventionTable(), andresolveByConvention(), andTypeBuilder’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 barescalar BigDecimalwith 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:ClasspathScannergains areadScalarConstantspass readingpublic static GraphQLScalarTypefields by exact JVM descriptor match (theJOOQ_CONDITION_DESCRIPTORidiom;finaldeliberately not required since the reflective resolver binds a non-final constant just as well), surfaced on a newCompletionData.ExternalReference.scalarConstantsslot (with a back-compat 5-arg constructor keeping ~20 test callers compiling);ScalarTypeCompletionsnow completesclassName.fieldNamefrom 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 enclosingscalar X. The sakilascalar BigDecimaland theGraphitronSchemaBuilderTestR355 fixtures gain explicit@scalarType; the twoCONVENTION_LAYERclassification cases are deleted, the single-pathUnclassifiedTypecontract case survives (asserting the new message so a re-added fallback fails loudly), and the oldDIRECTIVE_BEATS_CONVENTIONcase is retained reframed asDIRECTIVE_ALIASES_TO_DIFFERENT_CONSTANT. Coverage: scanner test pins the public+static+GraphQLScalarTypefilter with three negatives (wrong field type, non-static, non-public), rewritten LSP completion + text-edit tests feed a populatedCompletionDataand assert theclassName.fieldNameitems, 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, andcode-generation-triggers.adocdrop 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):CompileDependencyGraphBuildernow 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>Fetchersclass its<Type>Typeschema-shape wires (FilmMetaType -> FilmMetaFetchers); the builder registered only the schema-shape node, so the wiring loop never added the edge and theTypeSpecReferenceWalkcompleteness oracle (correctly) flagged the superset gap. Fix is a dedicatedaddNestedFetcherNodes()walk (called frombuild()beforeaddBlanketAndWiringEdges) that mirrorsTypeFetcherGenerator.collectNestedFetcherClasses’ reachability verbatim: iterate `TableBackedTyperoots, walk eachNestingFieldtree, dedup nested types by name, recurse into innerNestingField`s unconditionally, and register `units.fetchers(name)gated on a mirrorednestedTypeOwnsFetcherspredicate (noFetcherEmittercoupling, same discipline as the builder’sfiltersDecodeNodeId/hasSqlGeneratingFieldmirrors). Registering the node is the whole fix: the existing wiring loop then suppliesschemaShape -> fetcher,schemaClass -> fetcher, and the blanket edges for free. Coverage: unitCompileDependencyGraphBuilderTest.fetcherOwningNestingTypeRegistersFetcherNodeAndWiringEdgesand a pipelineIncrementalCompileHarnessTestcorpus extension (Film.meta: FilmMeta { language: Language @reference }, single-valued inlineTableFieldthat 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 oneFilmMetaType -> FilmMetaFetchersgap, 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, corec2e818b, cutoverc41752d, gate-fix376ecd8, In Reviewe83532b):@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 bareID/Boolean/ count and can never return the deleted row’s@tabletype (R287 rejects DELETE →@tableat authoring time, backstopped byMutationDeleteTableField’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.graphqlsgainstable: Stringon@mutation(documented DELETE-only);DmlWalkerInputArgResolutiongains aRawArgarm making "the single input arg is not aTableInputType`" a normal outcome rather than an immediate reject, with UPDATE translating it back to the pre-R457 rejection verbatim (byte-identical) via `rawArgUpdateRejectionwhile the DELETE classifiers own the fallback;FieldBuilder.resolveDeleteWriteTargetresolves the table by precedence and re-derives the input fields throughTypeBuilder.resolveInputFields(factored out ofbuildTableInputTypeso both routes share one classification loop, returning the narrow(fields | failure)fact, not a synthesizedTableInputType); the R330 validator-bypass is closed by mirroringGraphitronSchemaValidator.collectInputFieldRejectionsat the field-derived call site (a field-derived input never lands in the registryvalidateTableInputTypewalk);table:on a non-DELETE verb rejects loudly with the typed, sealedMutationTableArgError.UnsupportedVerb(stablegraphitron.mutation-table-arg.*LSP code), with the classifier andmvn graphitron:validatereading oneTABLE_ARG_SUPPORTED_VERBSset. 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-tierMutationTableArgClassificationTest(7 cases: byte-identical carrier vs@table-on-input, no-@tableclassifies, 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-tierFixtureWarningsGateTest(FilmDeleteInput warns naming@mutation(table:)), and thedeleteStorageBinByCoderound-trip inDmlBulkMutationsExecutionTest(the sakila fixture dropped@tableand set@mutation(table: "storage_bin"), round-trips identically against PostgreSQL); drift guardsRejectionSeverityCoverageTest+SealedHierarchyDocCoverageTest; no code-string assertions on generated bodies. Docs:mutation.adoc(signature, parameter table, "Naming the DELETE write target" section, reworked constraint bullet),table.adocWARNING,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(MutationTableArgClassificationTest7,TableOnInputDeprecationWarningTest4,FixtureWarningsGateTest2,DmlBulkMutationsExecutionTestround-trip all pass). -
R455 (
f1d93a8, workstream B319e668, In Review991c2ae): Fix theTypeSpecReferenceWalkblind 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$Largs that are a nestedCodeBlock/ anonymous-classTypeSpec/AnnotationSpec(a$Tstored opaque in a$Lblock 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 ($LString/$S). Workstream B (CompileDependencyGraphBuilder): a separate top-down projection walk mirroringTypeClassGenerator’s emit seam models the type-to-type projection-composition edges that were entirely absent (types.Film → types.Language` for each inlineTableField/LookupTableFieldcomposingTarget.$fields(...)), attributing every edge to the hosting type class so nesting-hosted fields land on the outer type (not their immediateparentTypeName()), reachingNodeIdEncoderprecisely when an inline filter decodes a@nodeIdargument, adding the generated<Type>Conditionsedge for aGeneratedConditionFilter, and blanketing the frozenGraphitronClientExceptionscaffold. The per-child dispatch is a no-defaultexhaustive switch over theChildFieldleaves 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 thetypeClass → NodeIdEncoder,→ conditions, and same-package nested-$Lprojection edges end-to-end. The fetcher-owning-nesting-type wiring gap the fix would otherwise surface is filed separately as R459. Coverage: 7TypeSpecReferencedClassNamesTestunit tests (nested$Lat depth ≥ 2, anonymous class, annotation, type/method type-variable bounds, recursive-bound termination, raw-string residual), 4 newCompileDependencyGraphBuilderTestunit tests (projection target,GraphitronClientExceptionblanket,NodeIdEncoderonly-when-decoding, nesting-hosted attribution to the outer class), and the extendedIncrementalCompileHarnessTest;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 Reviewe8571cc): GuardWorkspaceFilesource/tree reads against a concurrentdidChangeedit/swap/close. The LSP handed the live, mutableWorkspaceFileout of its lock (Workspace.get) and let the five async request handlers (hover, completion, codeAction, definition, inlayHint, all onForkJoinPool.commonPool) plus the diagnostic-recalc drain walk its tree-sitter tree and read itsbyte[] sourceon pool threads whiledidChangeedited 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 soNodes.textextracted garbage and hover/diagnostic ranges were wrong, and worst aWorkspaceEditcomputed 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 immutableFileSnapshot(tree, source, version)record carries ats_tree_copyclone (jtreesitter’s documented cross-thread mechanism) whose native lifetime is independent of the live file’s, so the eagerprevious.close()and in-placetree.edit()on the dispatch thread stay exactly as they were and cannot invalidate a reader’s clone;WorkspaceFile.snapshot()captures the triple under theWorkspacelock (the same lock the mutators run under, giving the happens-before edge, so novolatileon the plain fields).FileSnapshotis a deliberately distinct type fromWorkspaceFilewith 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.Workspaceexposes lambda-scoped accessors, not the snapshot’s lifetime:withView(uri, absent, present)snapshots one file (or short-circuits toabsent),withAllViews(present)snapshots every open file under one lock acquisition into an orderedMap<String, FileSnapshot>so a composed cross-documentWorkspaceEditis computed against one consistent generation of the whole workspace; both close every clone (including partials taken before the lambda throws) in afinally, making leak-by-omission structurally impossible (jtreesitter registers noCleaner, so an unclosed clone leaks native memory until process exit) and keeping the native lifecycle in the imperative shell. The live-file handoutWorkspace.getand the now-orphanedopenUrisare removed from the public surface;WorkspaceFileno longer escapesWorkspace. All reader call sites migrate: the five async handlers wrap theirsupplyAsyncbodies,publishDiagnosticsForRecalculatesnapshots per drained URI,CodeActions.computeandIntraSchemaDefinitions.computeusewithAllViewsfor a consistent cross-document generation, and featurecompute()signatures switch fromWorkspaceFiletoFileSnapshot(accessor names match, so mechanical); the completion self-tear is fixed since one snapshot now feeds its position, directive, and value reads.FileSnapshotdeliberately omitsdeclaredTypes()/dependsOnDeclarations()(consumed only byWorkspace’s own under-lock mutators, never off-thread). Coverage: `FileSnapshotConcurrencyTestis the named enforcer (plain JUnit ingraphitron-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) afterapplyEditandreplaceContenteager-close the original tree, close is independent in both directions, andwithAllViewspins one generation across a concurrentdidChange; the existing handler/feature tests are the behaviour oracle for the mechanical migration. Tests mint snapshots through a package-localWorkspaceFileTestSupportbridge to the package-privatesnapshot(). Alternatives rejected: an immutableWorkspaceFilebehind avolatile(needs a home for the reused incremental-parseParserand refcounting/Cleanerfor 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’sdidCloseclose()(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; fullgraphitron-lspsuite green (468 tests, 0 failures) under-Plocal-db. -
R452 (
723266d, Specc175917, Ready91310e7): Reject explicit@referenceand 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.branchParentFkWherereturnednullfor any non-single-hop-FK shape and the caller lowerednullto "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 pointFieldBuilder.resolveChildPolymorphicJoinPaths(all four producers: interface/union × table-backed/record-backed parent). Rule 1a rejects any field-level@referencestructurally (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 aDeferredcapability (participant table equals parent/hub →parsePathskips 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 sofkCountMessage’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/UnionFieldfrom a rawMap<String, List<JoinStep>>toMap<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, retiringbranchParentFkWhere’s `instanceof/null-for-unsupported arm andbatchedBranchJoinPredicate’s `(On.ColumnPairs) ((JoinStep.Hop) path.get(0)).on()blind cast (nullsurvives 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-tablevalidateSingleHopFkJoin, and the stalestub-interface-union-fetchers.mdpointer (R36 shipped) is dropped. Docs:polymorphic-types.adocandmultitableReference.adocnow state the rejection and point at the deferred capability. Coverage: new pipeline-tierMultiTableChildReferencePathRejectionPipelineTest(9 cases: condition/multi-hop/single-hop-{key:}@referenceall 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-emptyParticipantFkPath);TypeFetcherGeneratorTestfixtures migrated to the new carrier. Independent-session In Review → Done review; full reactor green under-Plocal-db -P!docs. -
R453 (
c529322, Spece9cb78d): Reject sort-enum values that declare neither@ordernor@indexinstead of silently skipping them. A partially-annotated sort enum bound to@orderBybuilt cleanly, contradicting the docs' promise of a per-value build failure (sort-results.adoc,order.adoc,orderBy.adocall state a missing value "fails the build with a per-value diagnostic"); at runtimeOrderByResolvercontinue`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 aResolved.Rejectedafter the loop, making the empty-ORDER-BY state unrepresentable in the model (namedOrderscomplete by construction). A newAuthorError.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 theRecordBindingMultiProducershape formessage()andprefixedWith. Per the Spec: the classifier’sanyMatchdetection 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-tierGraphitronSchemaBuilderTest(partial annotation → build error naming the unannotated value; two unannotated values → single accumulate-all rejection listing both), plus drift guardsRejectionSeverityCoverageTest(sample for the new arm) andSealedHierarchyDocCoverageTest(typed-rejection.adocparagraph + 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 Reviewdc70e62): 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 readparentType instanceof RootTypeand routed root multi-node chains toclassifyRootRoutineChain, so a@routinechain onMutationlanded aQueryRoutineTableFieldwhosesource()falsely assertsRoot.Query;FieldBuilder.classifyFieldnow reads the root position once (isRoot/isQueryRoot/isMutationRoot) and a Mutation@routine(multi-node in the interception, single-node fromclassifyMutationField’s top) lands a typed `Deferredsignpostingroutine-mutation-write(R451’s write arm, a capability gap not an authoring error), while Subscription and non-routine Mutation chains fall through to theirclassifyRootFieldstories. (D2) fold@routineinto both conflict detectors via a pairwise verdict table ; a sealedPairVerdict(Conflict|Deferred(planSlug)|Composes) thatreduceDirectiveConflictprojects over every unordered pair and reduces with Conflict-dominates-Deferred precedence, so@routine @lookupKey @servicerejects the@serviceconflict rather than short-circuiting to the@routine×@lookupKeydefer (the three-directive hole a slot-count carve-out would reintroduce);detectQueryFieldConflictis hoisted intoclassifyFieldbefore the interception and its old call insideclassifyQueryFielddeleted (one detector site per position). (D4) repointBuildContext.computeTerminalTargetVerdict’s `On.Lateralcomment atFieldBuilder.routineChainVerdict(theSplitRowsMethodEmitterjavadoc repairs stay R450’s scope). (D5) route the root routine fetcher (TypeFetcherGenerator) through the sharedRoutineCallEmitter.emitCallvia a new payload-freePreviousNodeRef.Nonearm andJoinPathEmitter.emitTableExpression, deleting the duplicatednonRoutineParamSourcehelper and inlineParamSourceswitch;QueryRoutineTableField’s compact constructor now pins every start binding to `ParamSource.Arg, making theNone×SourceColumnarm genuinely classifier-unreachable (construction throws before any emit), and the consolidation is byte-identical (rootcorrelatedis false, so noDSL.valwrap). (D3) three text-only R435 rejection fixtures gainisInstanceOfarm assertions (DirectiveConflictfor repeated@referenceonARGUMENT_DEFINITION;AuthorError.Structuralfor the input-field and element-less cases) plus a new fixture pinning the R300 single-node root desugar tohops = []. Coverage: 11 new pipeline-tier fixtures inGraphitronSchemaBuilderTest’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(503GraphitronSchemaBuilderTestcases, 0 failures). Out of scope: the routine write arm itself (R451), R450’sSplitRowsMethodEmittercorrelation rework, and lifting root position into a sealedRootType(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-columnTableRecordkey reconstruction calledClassName.bestGuess(col.columnClass())for every column inallColumns(), and for an array column jOOQ’sField.getType().getName()is the JVM binary descriptor ([Ljava.lang.Boolean;), whichbestGuessrejects, so any node type with an array-typed mapped column abortedgraphitron:dev/generatewithIllegalArgumentException: 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 aTypeName columnTypeonColumnRef/JooqCatalog.ColumnEntry, decoded viaTypeName.get(col.getType())(array-safe, mirroringRoutineParam) at the three reflection sites and threaded through every construction site; the 31ClassName.bestGuess(<ref>.columnClass())codegen sites read<ref>.columnType(). The rawcolumnClassstring is left in binary form intact for the consumers that depend on it (EnumMappingResolverClass.forName,SourceRowDirectiveResolverClass.getName()compares,GraphitronSchemaValidator), so this is a dual-fact split, not a replacement.ClassName-typed locals andDomainReturnType.Plainwiden toTypeName(safe:TypeName.equals/hashCodearetoString()-based and a scalar column still decodes to aClassName, 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@nodeNodeId key column (validateNodeType) or a DataLoader@splitQuerybatch key (validateFieldoverBatchKeyField.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 derivescolumnTypefrom a source-formcolumnClass(shared scalar-only decode,nullon placeholders, array columns can’t reach it) so ~161 hand-built fixtures stay unchanged;ColumnTypeConstructorArityGuardTestscans the main-source tree and fails the build if any production construction regresses to the string-decoding form. Coverage: unit-tierArrayColumnTypeDecodeTest(boundary decode:ClassNamefor scalars,ArrayTypeNameof the right element forboolean[]/text[],columnClassstays the binary descriptor), pipeline-tierArrayColumnCodegenPipelineTest(regression pin over a newarray_holderfixture through theTableRecordkey-extraction path, verified to reproduce the crash when reverted), andArrayKeyColumnRejectionValidationTest(@nodearray-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-decodetoString()), 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 Reviewe288670): Order-significant@routine/@referencecomposition ; a jOOQ table-valued function is a table node (R333’stableExprRoutineCall) that can be a field’s row source, its projected terminus, or sit between tables in the join chain. Both directives becomerepeatableand their written order defines the table chain (implicit head → contributions → terminus, last node == the field’s@table);ReferenceElementgains no arm and@referenceno new luggage, so composition is directive co-occurrence plus order, not a widened input (the rejected-alternatives log records why the@oneOfelement 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-axisHop(TableExpr target, On on)substrate, addsTableExpr.RoutineCall, the positiveOn.Lateralarm,On.Keying(ForeignKey|NameMatchedKey, the FK-less name-matched key for hops adjacent to a routine result),ParentCorrelation.OnLateralArgs, and aParamSource.SourceColumnarm so a routine IN parameter has exactly one source shape (argMapping→Arg,columnMapping→SourceColumn, the correlatedCROSS JOIN LATERALcase); every pre-existing sealed switch onOn/ParentCorrelationgained an explicit arm and the rootQueryRoutineTableFieldwas re-homed onto the(start, hops)chain (R300 desugars tohops = []). Shipped end to end at root and child positions: correlated single-node (lateral), routine-then-hops, hops-then-routine, sandwich, and repeated-@referencecomposed chains, through one chain walker shared by root and child classifiers; inline correlated multiset and the@splitQuerybatched keyed re-query on table-backed parents (batch key = the routine’s column-bound inputs). Typed classify-time rejections (root-head, terminus,columnMappingexistence/type/one-source, repeated@referenceoff FIELD_DEFINITION, Connection-terminus, uncorrelated-@splitQuery) each carry a validator projection and a fixture;@orderBy/@condition/ catalog-terminus-pagination /@lookupKeyland typedDeferred. Coverage: pipeline fixtures per chain shape inGraphitronSchemaBuilderTest’s R435 block, execution-tier proofs on a `films_for_actorfixture 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.adocrewritten for the composition surface and order contract. Remaining fetch-form breadth (multi-routine chains, record-backed /TableInterfaceTypeparents,@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 Review39ecbb9): Resolve a participant cross-table@referencecolumn read by the FK-pinned terminalTableRef(class identity), not a bare SQL name re-resolved through the catalog. Seventh audited site of the schema-qualified@tablebug class (siblings R396, R440, R441, R442, R422, R444), found by R444’s spec-time audit.TypeBuilder.extractCrossTableFieldsheld the FK-pinnedfk.targetTable()(identity-carrying since R441) and the resolvedinterfaceTable, but re-resolved their baretableName()strings throughJooqCatalog’s string lookups, which return `TableResolution.Ambiguouswhen 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 toFieldBuilder’s scalar `@referencepath where (post-R444) it misclassified as a plainColumnReferenceFieldinstead ofParticipantColumnReferenceField, 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@referencekey isTABLE__CONSTRAINTon the source table), and this path never routes throughServiceCatalog.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’sTableRef.column(String)matcher andTableRef.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 manualColumnEntry→ColumnRefconversion.extractCrossTableFieldsnow holds zero string-keyed catalog reads, one resolution story instead of two. Behavioral deltas: a colliding FK terminal now yields aParticipantColumnReferenceField(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-tierQualifiedParticipantCrossTableReferencePipelineTest(sibling of R444’sQualifiedTerminalReferenceColumnPipelineTestand R422’sQualifiedReturnTypeReferencePipelineTest, over the existing multischema fixture, no new DDL) ; a cross-schema-colliding FK terminal classifies green asParticipantColumnReferenceFieldwith 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@referencesub-class (R444 scalar + R445 participant cross-table); does not claim the whole schema-qualified@tableclass closed ; the author-qualifiable bare-name reads (ServiceCatalog.resolveColumn, path-start echoes) remain by design. -
R444 (
e5f3944, In Review3785229): Resolve a scalar@referenceterminal column read by the FK-pinned terminalTableRef(class identity), not a bare SQL name re-resolved through the catalog. This was the sixth site of the schema-qualified@tablebug 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@referencecolumn read was a separate unaudited path with the identical defect.ServiceCatalog.terminalTableSqlNamewalked the FK path and returnedhop.targetTable().tableName(), collapsing the identity-resolved terminalTableRef(which R441 populated with atableClassidentity) to a bare name string;resolveColumnInTablethen re-resolved it throughJooqCatalog.findColumn(String, …), which hitTableResolution.Ambiguouswhen the terminal table name collides across generated schemas and demoted the field toUnclassifiedFieldwith a spurious "column could not be resolved" author error and no author-side workaround (the@referencekey 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"): replaceterminalTableSqlName/terminalTableSqlNameForReferencewith the ref-carryingterminalTableForReference(List<JoinStep>, TableRef)(walk the path, terminal is the last hop’stargetTable(), empty path yieldsstart, condition-only steps still bail to empty); retyperesolveColumnForReferenceto take the start asTableRefand resolve the column off the terminal ref via newTableRef.column(String); the single model-side matcher home mirroringfindColumn’s order (javaName`equalsIgnoreCaseacross all columns, thensqlName), never a bare-name catalog re-resolve. The scalar-output unknown-column diagnostic now enumerates the terminalTableRef.allColumns()java names (previously empty on a colliding terminal, ambiguity-broken too); the argument-filter (FieldBuilder) and input-field (BuildContext) sites pass their already-resolvedTableRef. Behavioral deltas: a colliding terminal now resolves; condition-only paths, unknown columns, and empty paths keep today’s outcomes. Coverage: pipeline-tierQualifiedTerminalReferenceColumnPipelineTest(sibling of R422’sQualifiedReturnTypeReferencePipelineTest, over the existing multischema fixture, no new DDL) ; a@referenceread on the cross-schema-colliding FK terminal classifies green asChildField.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-@referencescalar read (source@tableecho resolves qualified, author has a workaround) and the participant cross-table@referencepath (a genuine seventh site, no workaround, tracked as R445, which consumes this item’sTableRef.columnmatcher). Independent-session In Review → Done review; full graphitron suite green (2526 tests) under-Plocal-db. Closes the FK-terminal@referencecolumn-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 Reviewa1edbe0): Compare return-type identity in the@referenceterminal-target verdict, not the verbatim@tableecho.BuildContext.computeTerminalTargetVerdictdecided whether an@referencepath’s terminal hop lands on the field’s return-type table viaTableRef.sameTable(a bareequalsIgnoreCaseagainst the return type’s verbatim@tablestring), so a schema-qualified return@table(e.g.multischema_a.widgetwhile the hop resolves to jOOQ’s unqualified canonicalwidget) spuriously reportedMismatchand demoted the field toUnclassifiedField. Last open member of the schema-qualified@tablebug class after R396 (source-side FK predicate), R440 (FK-join endpoint/FK identity), R441 (landedTableRef.denotesSameTableAs), R442 (condition-param match). Chosen design (settled at Spec against a fullparsePathString→identity migration): thread the already-resolved return-typeTableRefintoparsePathas its own nullable axis alongside the existingtargetSqlTableNameString ; 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’sTableRef.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 inFieldBuilder, plusNodeIdLeafResolverwhich hoists itsfindTableaboveresolveFkJoinPathso ref and name pass together); the return-side null gate moves toreturnTableRef == nulland theMismatchmessage 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-tierQualifiedReturnTypeReferencePipelineTest(sibling of R396’sQualifiedSourceReferencePipelineTestover the multischema jOOQ fixture, no new DDL) ; the schema-qualified return@tableclassifies green asChildField.TableFieldand the terminal hop genuinely lands onwidget, paired with a genuine mismatch (return type bound toevent, hop lands onwidget) that still rejects toUnclassifiedField; no code-string assertions.TableRef.denotesSameTableAsalready pinned by R441’sTableRefSameTablePredicateTest, no new predicate coverage. Independent-session In Review → Done review; full reactor green under-Plocal-db. Builds on R396/R441; closes the schema-qualified@tablebug class. -
R440 (
d0bd8ca, Spec6cfda86/426c586): Resolve FK-join synthesis endpoints and the FK itself by jOOQ class / reference identity, not bare SQL name.BuildContext.synthesizeFkJoinalready held the jOOQForeignKeyobject (whose endpointTableclasses 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 yieldedTableResolution.Ambiguousand 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@tablebug 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) newJooqCatalog.findForeignKeyRef(ForeignKey)resolves theKeys-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;fkJavaConstantNameretargeted onto the FK object; the false-docstringfindForeignKeyByNamedeleted. (D2)synthesizeFkJoinresolves both endpoints viafindTableByClassoff the FK, soUnknownTablebecomes 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) theOptional-returningfindForeignKey(String)(which could only collapse a collision into "not found") replaced by a scoped, sealedfindForeignKey(name, sourceSqlName)returningForeignKeyLookup(Resolved/NotInCatalog/Ambiguous); the three author-facing sites ({key:}path element, IdReference synthesis shim, explicit@reference(key:)record-FK) rejectAmbiguousthrough a newambiguousForeignKeyRejectionstructural builder naming the colliding schemas + qualified forms, while the non-author-facingqualifierForFkmigrates keeping itsOptionalcontract. (D4)findUniqueFkToTablereturnsOptional<ForeignKey>andNodeIdLeafResolverconsumes the object directly, dropping the name round-trip that re-collided afterfindForeignKeysBetweenTableshad already resolved by class. Fixture: anotetable in both multischema schemas, each with an FK namednote_event_fkinto its own schema’sevent, giving both a colliding bare target-table name and a colliding FK constraint name, plus seed rows for execution-tier reuse. Coverage: unit-tierJooqCatalogMultiSchemaTestD1-D4 cases (per-schemaKeys-class ref resolution, endpoint-by-class from each side, scoped disambiguation, null-scopeAmbiguousnaming both schemas, structural-prose rejection) plus the{key:}membership enforcerparsePathElement_keyNotTouchingSource_rejectsBeforeSynthesis;findUniqueFkToTabledirectionality cases migrated to the FK-object shape inJooqCatalogIdRefTest; no code-string assertions on generated bodies. Resolver-tier wiring judged disproportionate (nodeid fixtures stay single-schema; the collision lives at the catalog/BuildContexttier), recorded in the item.ForeignKeyLookupis aJooqCatalog-local result type in theTableResolution/ForeignKeyResolution/RoutineResolutionfamily, out of scope forVariantCoverageTest/SealedHierarchyDocCoverageTest; the ambiguous arm produces an existingRejection.structuralleaf, adding notyped-rejection.adocobligation. Independent-session In Review → Done review; full reactor green under-Plocal-db. Builds on R396/R438; sibling of R441/R442/R422. -
R442 (
87d25a9, Specb9c8cfa): Make the R379 Check-2 concrete-condition-param table match compare by jOOQ class identity, not a bare-vs-qualified name string.BuildContext.checkConcreteParamTablecompared the parameter’s bare jOOQ table name (event) against the hop’s possibly schema-qualified@tableecho (multischema_a.event) withequalsIgnoreCase, so in a multi-schema catalog a concrete@conditionparameter typed with the correct generated table class was false-rejected (author’s only workaround: widen toTable<?>, discarding the type safety), and two same-named tables in different schemas were indistinguishable. Another member of the schema-qualified@tablebug class (R396 done, R441 the accessor-side sibling landed just prior, R422/R440 Backlog); surfaced from gap D of theopptakmulti-schema migration. Design shape 1 (chosen over re-resolving the string inside the check): thread the resolvedTableRef`s down the validator chain instead of name strings. `validateConditionParamTables/checkConcreteParamTablenow takeTableRef source/target; the condition-hop site threads the hoistedconditionOrigin(null when the source is not table-backed, the existing skip) andr.target(), the where-filter site threadshop.originTable()/hop.targetTable(). The final compare isTableRef.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-tierMultiSchemaConditionParamTest+MultiSchemaConditionStubover the colliding-eventmultischema 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. Additivemultischema_a.event_logDDL (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 resolveseventby bare name throughsynthesizeFkJoin(ambiguous across the two schemas), which is R440’s scope, not R442’s. Independent-session In Review → Done review; fullgraphitronsuite 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 (
e0b878aimplementation; Backlog → Spec62d1222, Spec → Readyd5117fa, In Progress9d3756c, In Review2fdb708; 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@tablename. Same multischema migration as R396/R422’s family (gap E ofgraphitron-qualified-names-gaps): once an element type’s@tableis schema-qualified to disambiguate a cross-schema bare-name collision (multischema_a.eventvsmultischema_b.event), the verbatim@tableecho neverequalsIgnoreCase’d jOOQ’s always-unqualified canonical `event, soFieldBuilder.collectAccessorMatchessilently dropped the accessor and the payload parent rejected withRecordTableField … requires a typed accessor or @sourceRowplus cascadingWrapperArm errors transportfailures on the siblingerrorsfield. Fix routes the comparison through the identity already in hand:TableRef.denotesSameTableAs(TableRef)compares the carriedtableClass(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 viaJooqCatalog.TableEntry.toTableRefalways populate it, so production always takes the identity arm);collectAccessorMatchesthreads the expectedTableRefinstead of its bare SQL name and filters viadenotesSameTableAs, 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 onJooqCatalogper R396 vs. model-side reifiedClassNamehere) 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,FieldBuilderhop-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-tierTableRefSameTablePredicateTestpins all three arms (same class/divergent names → true; same bare name/different class → false, the silent-regression guard; classless fallback); pipeline-tierTypedAccessorSchemaQualifiedIdentityPipelineTestover the multischema fixture pins both directions (qualified echo classifies green asRecordTableFieldwith accessor-derived source and schema-AtableClass; different-schema record dropped and field rejects), asserting classifier outcomes andsourceKey, no code-string assertions on generated bodies.TableNameComparisonCaseGuardTestuntouched (identity route lives in the guard’s excludedPREDICATE_HOME). Full reactor green under-Plocal-db. -
R438 (
materialize-joinpath-facts; slices3754f40JoinConditionRef,8106991axes minted,099aa30producer/reader cutover,d3dafa3flat-variant delete,b0ab513self-review fixes; independent-session In Review → Done review):JoinStepreshaped onto R333’s two orthogonal axes ;Hop(TableExpr target, On on, originTable, JoinConditionRef filter, alias)withOn.ColumnPairs | On.Predicateand the day-oneTableExpr.Catalogarm; the flatFkJoin/ConditionJoinvariants and theWithTargetcapability deleted, slot iteration now the standaloneHasSlotscapability shared byOn.ColumnPairsand the transitionalLiftedHop(both retired by R431). Absorbed and closed R16. Generated output byte-identical per slice; gate review re-verified the full reactor green under-Plocal-dband fixed one staleWithTargetmention inemitter-conventions.adoc. -
R16 (
fkjoin-model-cleanup; absorbed and closed by R438’s slice 1 + cutover): the join-condition calling convention is now typed.JoinConditionRefwraps theMethodRefpopulation called asmethod(srcAlias, tgtAlias)byJoinPathEmitter.emitTwoArgMethodCall, which takes the wrapper directly so call sites stop extracting rawMethodRef`s; handing a `WhereFilter-convention method to a join-condition emit site is a compile error. R16’swhereFilternaming complaint dissolved structurally in the R438 cutover: the ON-clause condition and the WHERE-appended filter became differently-named components (On.Predicate.conditionvsJoinStep.Hop.filter) instead of one overloadedwhereFilter. -
R439 (
d34ef37implementation; filed66dea39, Backlog → In Reviewea67d6cat 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, PostgreSQLrewrite_testdrop/reseed, Maven settings de-proxy, libtree-sitter 0.26.9) in the background, then warms the whole reactor withmvn -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 leavesfailed, 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.jsonwith a 2700s timeout) extracts the command from the hook JSON and holdsmvncommands through both phases andpsqlcommands 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 persistsJAVA_HOMEthrough$CLAUDE_ENV_FILEso agent shells stop inheriting a staleJAVA_HOME=java-21past 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 (
3b38bebimplementation; Spec → Readyee39933, In Progress → In Review3cac680; 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 bydocs/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 todocs/architecture/reference/emitter-conventions.adoc(plus a new cursor encode/decode section); the dunder-rule full semantics moved toGeneratedSourcesLintTest’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 newDocSizeBudgetTest.developmentPrinciplesStaysUnderBudget. Citation sweep retargeted every live reference (docs xrefs with changed section anchors, javadoc,.claudeprompts,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 (
420cd32implementation,d905facrework 1,2ba655arework 2; Ready sign-off under the user’s in-session short-circuit, In Review → Ready gate 1ad18fa5, In Review → Ready gate 2aa6fe29):docs/architecture/explanation/rewrite-design-principles.adocviolated 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 (theGeneratorCoverageTest-guarded dispatch partition is the kept exemplar); otherwise state the rule, one canonical exemplar, and the smell. Applied acrossSourceKey.Reader/Wraparm enumerations, the four-axisSourceKey/LoaderRegistrationcensus, two parse-boundary file censuses, theCallSiteExtractionstrategy list, thecandidateHintoccurrence census, the helper-locality compliance roster, and the R50 retired-carrier roster; vision-alignment forward notes added pointingSourceKeyat R431 andMethodBackedFieldat its R222 retirement (ServiceFieldas the current capability exemplar); staleArgCallEmitter.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 retiredTextMapLookuppermit (R229), an 11-of-12 module census, an unguarded "thirteen resolver siblings" count, and an over-inclusivejava.lang.reflect-import discovery recipe (keyed ontoType-tree reads instead), plus same-family ride-alongs incode-generation-triggers.adoc,argument-resolution.adoc,typed-rejection.adoc,SealedHierarchyDocCoverageTest, andSourceKey.Reader’s javadoc; gate 2 caught a sentence-initial "Thirteen directive resolvers" in `typed-rejection.adocthat survived gate 1’s case-sensitive grep sweep, fixed with a case-insensitive re-sweep (zero remaining hits outside an unrelatedPGThirteentest 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-dbat each gate. -
R436 (
2992d25implementation; Backlog → Spec20b5072, Spec → Ready97c0bb3, In Progress → In Reviewe263020): Fix unsafeinto()key extraction colliding with multiset aliases and escaping error redaction. Two defects on a@service/@splitQuerysplit field over a@tableparent whose DataLoader key wrap isSourceKey.Wrap.TableRecord. Defect 1: the key read did a whole-recordenv.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/tiderover range columns; the sakila repro:Film.LengthoverFILM.LENGTHsmallint) poisoned the conversion and threw aMappingException, 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.exceptionallyrouter, so it escapedDataFetcher.get()unrouted and leaked jOOQ’s raw record-dumping message pastErrorRouterredaction (a privacy hole; per-node repetition blew the OTel gRPC 4 MiB export limit). Fix (Defect 1): the parent$fieldsprojects the full row under reserved__src_<col>__aliases (GraphQL reserves leading-__for introspection, so no client-driven sibling alias can collide) andGeneratorUtils.buildKeyExtraction’s `TableRecordarm rebuilds the typed record column by column with explicit types (no runtimeField<?>loop / unchecked cast). Both emit sites drive off a new generation-timeTableRef.allColumns(populated inJooqCatalog), single-homing the projected names and the extraction’s lookup names so they cannot drift.RequiredProjectionreshaped 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 theWrap.Row/Wrap.Record/TableMethodFieldreads 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, mirroringemitSelectionSwitch) 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.buildwraps extraction + dispatch + async tail intry/catch (Throwable)routing through the sameasyncRouterCalldisposition the.exceptionallyarm uses (single-homed inTypeFetcherGenerator, threaded to both arms so they cannot diverge), lifted into a completed future; the R268preRegistrationPreludestays outside the guard by design. Coverage: execution-tierGraphQLQueryTest.films_titleTitlecase_withCollidingMultisetSibling_bothResolve_noMappingException(both fields resolve post-fix), pipeline-tierServiceProjectionPipelineTesttwo-axis emit +AliasKeyColumnCollisionValidationTest(rejection + no-false-positive), unit-tierDataLoaderFetcherEmitterTestguard-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.adocextraction 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 viaFilm.durabilityError. Independent-session In Review → Done review; full reactor green under-Plocal-db(all 13 modules incl. thegraphitron-sakila-exampleexecution + Java-17 compile tier). Builds on R426/R425/R415/R268. -
R437 (
f293803implementation; Backlog → Spec4119cf7, Spec → Readyd8c8653, Ready → In Progress0fef51c, In Progress → In Reviewe41a4d8): Shape-awarecreate<Record>/create<Record>List@servicehelper dedup, fixing an R311/R315 silent-column-drop correctness bug. The jOOQ-TableRecord@serviceparam helpers were deduplicated by record class alone (putIfAbsent(recordClass, …)at two collection sites, and both call-site namers derived fromrecordClass.simpleName()), so two@servicefields on one type binding the same record through different input shapes (different@fieldcolumn 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 infs-plattform’s `registrerCampusForUtdanningsmulighetvsdeaktivereCampusForUtdanningsmulighet, whereDATO_FRAwas dropped to1900-01-01). The fix re-keys dedup, naming, and call-site routing by the full binding shape (record class + orderedColumnBinding`s + ordered `RecordKeyDecode`s): a new `JooqRecordHelperNamesresolver dedups on the carrier’s own structuralequals(D1 ; exactly "these two emit an identical helper body", so no parallel signature function can drift), names uncontended record classes with the barecreate<Record>(byte-identical to pre-R437, no churn) and contended ones withcanonicalRender-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>Fetchersclass (both coordinates) and stashed onTypeFetcherEmissionContextbefore any field body emits, so the helper drain and both call-site emitters (ArgCallEmitterchild,ServiceMethodCallEmitterroot 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 defaultbare()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-tierJooqRecordServiceParamPipelineTestR437 group ; contended-singular red regression (two distinct helpers, each fetcher routes to its own, exactly one setsRELEASE_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-dbincl. thegraphitron-sakila-exampleJava-17 compile (no generated-output churn). Builds on R311/R315. -
R410 (
e7ae955slice 1,1fae7e9slice 2,55e04e3slice 3,6149cfeslice 4,a0e6950slice 5,d77fd0cslice 6; review fixes in the In Review → Done commit range):graphitron:devowns incremental compilation of generated sources. The dev loop now turns generated.javainto.classin-process, into the graphitron-exclusivetarget/graphitron-classes(sole-writer dir, first-on-classpath precedence for consumers that load external.class;quarkus:devempirically 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);CompileDependencyGraphis projected from the classified model through exhaustive switches over theGraphitronType/GraphitronFieldleaves mirroringprojectFieldClassification, with frozen-vs-growingUtilSingletonclassification so blanket over-approximation never harms pruning (slice 2);AbiSignaturehashes the signature surface (constant values included for javac inlining; type variables with bounds added in review) andRecompileSetcomputesdelta ∪ ABI-changed reverse-transitive dependentsas pure functions (slice 3); a warmJavaCompiler/StandardJavaFileManagerengine with per-round fresh tasks, orphan.classsweep, and a dedicatedCompileDiagnosticchannel kept separate fromValidationReport(slice 4); the two-clause acceptance harness (incremental tree byte-for-byte equals clean full compile; body-edit prunes / ABI-edit propagates) plus theTypeSpecReferenceWalkcompleteness oracle (slice 5);DevMojowiring with-Dgraphitron.dev.compile=falseopt-out, no fail-fast (exclusive dir degrades safely), conservative whole-tree recompile on consumer.classchange, and compile diagnostics surfacing through the console block (CompileErrorFormatter) and the MCPdiagnosticstool with asource: "schema"|"compile"discriminator (slice 6). Independent-session In Review → Done review landed four fixes: the completeness oracle widened with a@nodetype, which falsified missing node-lookup wiring edges (QueryNodeFetcher/EntityFetcherDispatchincluding the per-node-typetypes.<T>projection references andentitiesByTypefederation 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.classlingers; a first recompile with no ABI baseline (skipInitial) establishes the full image instead of a half-populated dir;AbiSignaturegained type-variable/bounds coverage andDevMojo.lastGenerationbecame 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 (
3931908implementation; Backlog → Specccbaea5, Spec → Readyb774532, In Progress → In Review0d01fc1): Project the full parent row for TableRecord-sourced@servicechildren, honoring the already-documented contract of the typed-TableRecordsource shape. A@servicechild whoseSourcesparameter is a typedTableRecord(Set<FilmRecord>,SourceKey.Wrap.TableRecord) receives keys viaenv.getSource().into(Tables.X), but the parent$fieldsSELECT 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 selectedtitlealongside, and the federation_entitiesshape (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 sealedRequiredProjection { FullParentRow | Columns }with an absorbing combine (FullParentRowdominates; "full row subsumes columns" is a type fact, not a dedup accident);build$FieldsMethodswitches once, the full-row arm emitting a singleCollections.addAll(fields, table.fields())append that is alias-correct by construction (the caller’stableparam carries base column names, the same namesinto(Tables.X)reads by). Gated on the key wrap (SourceKey.Wrap.TableRecord), not the sealed field variants, so any futureBatchKeyFieldacquiring the wrap gets the right projection for free; sits after R425’s record-parent guard so only table-parent fields reach it. Coverage: pipeline-tierServiceProjectionPipelineTestR426 group (full-row append forServiceTableFieldandServiceRecordFieldTableRecord-sourced children via the newTypeSpecAssertions.appendsFullParentRowhelper, aRecord1-sourced contrast sibling pinning the wrap gating, and aNestingFieldrecursion case); execution-tierGraphQLQueryTest.films_titleTitlecase_withoutSelectingTitle_readsNonKeyColumnOffSourceRecordunmasks the in-tree reproducer ({ films { titleTitlecase } }with notitleselected, red pre-fix); federation execution-tierFederationEntitiesDispatchTest.entities_tableRecordServiceChildOnly_nonKeyColumnReadResolvesNonNull(representations-driven fetch, non-key read). Docs:handle-services.adoccontract 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 (
7400c67core,1a855a4rework; Backlog → Spec889f6cb, Spec → Readyc205824, In Review → Ready rework814e255, In Progress → In Reviewa5c485f): Route inline (non-@splitQuery)@referencefield argument reads through the field’s ownSelectedFieldinstead of the ancestor fetcher’s env. Inside the generated<Type>.$fields(sel, table, env)method,envbelongs to the top-level operation fetcher, soenv.getArgument("filter")returned null, the filter condition collapsed tonoCondition(), and the field silently returned unfiltered rows (data-correctness bug, discovered via an opptak-subgraph reproducer where a@nodeIdfilter onStudiekurv.kladderwas ignored; the@splitQuerysibling behaved because its env genuinely is the field’s own). Fix threads a sealedArgumentValueSource(Env|FromSelectedField(sfLocal)) throughFkTargetConditionEmitter.emitTerm→ArgCallEmitter.buildCallArgs→buildArgExtraction; root/split sites passEnv(byte-identical output), the two inline emitters passFromSelectedField(sfName)so runtime reads resolvesf.getArguments().get(name). Covers the filter-condition path, the inlinefirstpagination limit, and theJooqConvertlist pre-lift (`+emitJooqConvertKeyLifts, added to both inline emitters by parity ; pre-R424 that inline shape emitted a reference to an undeclared `<name>Keyslocal; the helper takesFromSelectedFielddirectly since it has noEnvcaller).ContextArgstays env-based (request-scoped context is legitimately the ancestor env); never-inline arms (InputBean/JooqRecord) guard withIllegalStateException. The$fieldshost stamps@SuppressWarnings("unchecked")via the source-awareCallParam.emitsUncheckedCastFromSelectedField, keeping theEnvhosts' warning-free output unchanged. Coverage: pipeline-tierInlineFilterArgumentSourcePipelineTest(source-aware suppression stamp incl. the top-levelJooqConvertlist pin over the non-`@node+`store → customerFK, scalar negative case, end-to-end generation; annotation/model assertions only), execution-tierGraphQLQueryTestagainst real PostgreSQL (Store.customersByFirstName{,Split}narrowing + inline/split parity,customersFirstNlimit, and the decode-consumingCustomerByNodeIdFilterfixture where a foreign store’s node id narrows the inline child list to empty with a@splitQueryparity mirror); compilation tier via the sakila-example-Werrorbuild. Independent-session In Review → Done review; full reactor green under-Plocal-db. -
R425 (
9c2c9abimplementation; Backlog → Spec119bfb2, Spec → Ready2969866, In Progress → In Review70484cd): Force-include a@service/@splitQuerychild’sSourceKeycolumns in the parent$fieldsprojection so its DataLoader key is never silently null. A@splitQuerychild 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_entitiesfetch supplies keys viarepresentationswithout re-selecting them. Root cause was a pattern-match omission:TypeClassGenerator.collectRequiredProjectionColumnsmatched the twoSplit*arms (so split-@referencechildren were already covered) but letServiceTableField/ServiceRecordFieldfall through toStream.empty(). Fixed by collapsing the twoSplit*arms into a singleBatchKeyFieldcapability arm returningsourceKey().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 loudIllegalStateExceptionrather than routed: they key off a Java accessor viabuildRecordParentKeyExtractionand 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 nullSourceKey(a@servicemethod taking no Sources param, a plain per-parent delegation with no key read) contributes no columns rather than NPEing. Fold-ins:BatchKeyFieldjavadoc gains the missingRecordTableMethodField; the two force-include taxonomy comments name the capability instead of enumeratingSplit*. Coverage: pipeline-tierServiceProjectionPipelineTest(table-bound returnServiceTableField, scalar returnServiceRecordField, and a service child nested under aNestingField, each on a parent with no other force-projecting child, assertingTypeSpecAssertions.appendsRequiredColumnon the parent PK); execution-tierGraphQLQueryTeston a new unmaskedCityfixture (no@splitQuery/@tableMethodsibling, so the@servicechildren are the only reasonCITY_IDis projected) coveringcityUppercase(Wrap.TableRecord, the silent-null shape) andcityLowercase(Wrap.Row, the loud-throw shape) queried without any key-mapped field, backed by the newCityService; federation execution-tierFederationEntitiesDispatchTestwith a representations-driven_entitiesfetch selecting only the service child (the opptak reproducer shape,Citypromoted to a@keyentity,FederationBuildSmokeTest’s `_Entityunion pin updated); plus a note on the existingFilmservice-child tests thatcast/castByKeymask this behaviour forFilm. No code-string assertions on generated method bodies (pipeline tier uses the spec-namedappendsRequiredColumnshape helper). Developer-sideWrap.TableRecordcontract hazard filed separately as R426; distinct from R424 (child arguments read from the wrongenv). Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db. -
R421 (
fb4ca33guard + tests + pom + seam;8337c14test references the seam’sFAULT_HEADERconstant;44fc1b0move fault seam to a test@Alternative+ pin the true single contract; Specbe489aa/514e2f0, Spec → Ready9927eb8, In Progress → In Review83d792a, spec wording correction6ac9f29): StopGraphqlResource.execute()ingraphitron-jakarta-restfrom leaking internals when the server-side execution path throws. The resource shaped every request error (400/405/422) into a spec-compliantapplication/graphql-response+jsonbody but ran the consumer-implemented, auth-seededapplication.newExecutionInput()SPI seam andengine.execute()unguarded; a fault there (observed: the seam forcing a JDBC connection with the DB down, aCreationExceptionwrappingPSQLException) 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:WebApplicationExceptionis aRuntimeException, so the broad arm would otherwise swallow it);catch (Exception)mints aUUIDcorrelation 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 generatedErrorRouter.redactemits (ErrorRouterClassGeneratorline 481). This resource-level guard is the structural complement toErrorRouter’s per-fetcher redaction: `newExecutionInput()runs before graphql-java execution begins, the one region neitherErrorRouternor 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’sGraphqlErrorBuilderwhich serialises a defaultextensions.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 isorg.slf4j:slf4j-apiatprovidedscope (already version-pinned in the parentdependencyManagement);jakarta.ws.rs.WebApplicationExceptionrides the existingprovidedjakarta.ws.rs-api, and no RESTEasy/Quarkus type is named. Testing lives ingraphitron-sakila-example’s `GraphQLOverHttpConformanceTest(R399: thegraphitron-jakarta-restmodule carries no@Testclasses of its own): four R421 cases (redacted 500 modern with no leaked internals, redacted 200 legacy,WebApplicationException→ 403 passthrough, andredactionShapeMatchesFetcherPathpinning message-identity on both legs / no-extensions on the resource leg / classification-present-but-clean on the fetcher leg via the existingFilm.durabilityError@serviceleaf). Fault injection is a test-scoped@Alternative @Prioritybean,FaultInjectingGraphitronApplication, which subclasses the real adapter and throws on a sentinelX-Graphitron-Faultheader while delegating every other request tosuper.newExecutionInput(), soexecute()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 OTeltrace_idvia MDC, rather than a fresh UUID) was deliberately deferred to Backlog R423. Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db(GraphQLOverHttpConformanceTest15/15, 0 failures across the reactor). -
R408 (
352b05bcimplementation, option A; In Progress → In Review599cbc28, Spec → Readya3564f2a): 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@lintDisabledirective (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).LintBindingcollapses the POM block into aLintConfig(Set<String> disabledRuleIds, List<String> excludedTypePatterns)record onRewriteContext;LintConfig.validatedtypes each disabled id againstLintRule.ids()and fails the build (viaMojoExecutionException) 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 combinedBuildWarninglist after classifier advisories (schema.warnings()) and engine findings are concatenated, so it keys on the typed rule id and covers both channels (aSource.CLASSIFIERadvisory likesplitquery-redundant-on-record-parentis suppressible by id like any engine rule); the type-name-glob filter runs insideLintEngine.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 anexcludedTypes-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 theValidationReportand the MCPdiagnosticstool projects it, andDevMojobuilds its context through the samebuildLintConfigseam, 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@lintDisabledirective, no plugin SPI for new rules. Coverage: pipeline-tierLintSuppressionPipelineTest(5: disabled-rule drops that rule while others fire,excludedTypesskips the matching type but not siblings, glob match, classifier-advisory-by-id suppression, and the engine-scopedexcludedTypesasymmetry), unit-tierLintConfigTest(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 livediagnosticstool); assertions key on the typedLintRule/lintRulewire 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 undermvn install -Plocal-db. Builds on R398 (build-is-single-evaluator spine, typedLintRuleid); the "author chooses not to fix" sibling of R407’s "cannot fix" exclusion, landing at the sameLintEngine.runboundary. -
R396 (
2509653implementation; In Progress → In Reviewe79a746, site-6 Spec review0aeb581, Spec revise7082fa5, Backlog → Spec5ff535b): Accept schema-qualified or case-mismatched@table(name:)base names on the@referenceFK-connection-and-orientation path. A type declared@table(name: "multischema_a.signal")(ormultischema_a.SIGNALover the real lowercase name) could not attach an@reference(path: [{key: "<fk>"}])field: the verbatim, case-preserved@tableecho was compared by bareequalsIgnoreCaseagainst 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@referencesibling of R395. Fixed by identity comparison, not input sanitization: two newJooqCatalogprimitives,foreignKeyTouchesTable(source-side membership, either endpoint) andforeignKeyOnSource(orientation; self-referential FKs fall to the caller’sselfRefHint), resolve the source through the schema-awarefindTableand compare FK endpoints by jOOQ table class identity (endpoint.getClass() == resolvedSource.getClass()), falling back to the historical bare compare when the source isAmbiguous/NotInCatalogso 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)parsePathElementconnection check →foreignKeyTouchesTable; orientation decided once viaforeignKeyOnSourceinsynthesizeFkJoinand threaded intoresolveFkSlotsas a precomputedboolean 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)findForeignKeysBetweenTablesresolves each argument to class identity;findUniqueFkToTable(site 4) andqualifierForFk(site 6, the synthesis-shim path whose bare re-filter turned a qualified-@tableinput type into a hardIllegalStateException) re-filter throughforeignKeyOnSource. One documented deviation, endorsed on review: site 6 usesforeignKeyOnSource(…, selfRefHint=true)rather than the spec’s suggestedforeignKeyTouchesTable, preserving the method’s strictly source-side semantics and the existingqualifierForFk_wrongSourceTable_returnsEmptytest. 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 viaTableRef.sameTable) split out to R422 per the spec’s own scope recommendation. Coverage: unit-tierJooqCatalogMultiSchemaTest(both primitives oversignal_widget_id_fkeywith qualified and upper-case sources, referenced-side/non-endpoint/cross-schema-same-name cases, qualified-both-argsfindForeignKeysBetweenTables, and asynthesizeFkJoinqualified-source orientation guard pinning origin=signal/target=widget and slot orientation); pipeline-tierQualifiedSourceReferencePipelineTest(all three@referenceforms plus the qualified-and-upper-case spelling classify to a correctly-orientedFkJoin, no author error); execution-tierMultiSchemaQueryTestwith the R395 fixture tightened from@table(name: "signal")to its originally-specified@table(name: "multischema_a.SIGNAL"), rows still routing to the discriminated types andAlertSignal.widgetNamepopulating 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 atf6cc9aebefore R396 entered In Progress at3518bb9); spawns R422. Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db(unit 55, pipeline 4, execution 4 in the touched suites; 0 failures across the reactor). -
R418 (
8213374implementation; In Progress → In Review2eb9752, Spec → Ready8d80d72, Backlog → Spec440b207, Backlog146d334): Make the web-sandbox SessionStart hook drop-and-recreaterewrite_teston every start, so the fixture DB is a pure function of the checked-outinit.sql. Step 2 of.claude/scripts/session-start-web-env.shpreviously created + seededrewrite_testonly when the database was absent; a sandbox first seeded from an olderinit.sql(e.g. one predating R389’sparty_*joined-table fixtures + thejti_*composite PKs) kept that stale schema forever, and-Plocal-dbjOOQ codegen then built its catalog against the stale DB, cascading into theUnclassifiedType/UnclassifiedFieldfailures (Query.allParties,JoinedTableInheritancePipelineTest, and siblings) that every recent Done review documented as a manual re-seed. The existence guard is replaced by an unconditionalDROP 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 unchangedpg_ctlcluster/pg_isreadygate so local dev (TestContainers, no persistentrewrite_test) stays a no-op; the password-reset line and itspg_was_runningguard are untouched, and no checksum/skip optimization is added (sub-second reseed, and a skip-guard would reintroduce the staleness window)..claude/web-environment.mdupdated: 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 orinit.sqlchange; the R389jooq.codegen.schema.versionbump is orthogonal and untouched. Verification is manual shell testing (spec § Verification):bash -nparses; 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 (JoinedTableInheritancePipelineTest5/5,VariantCoverageTest3/3 green under-Plocal-db). This item is the standing fix R413/R414/R384/R415/R407/R182 named for the recurring stale-rewrite_testreview artifact. -
R413 (
0d4a2d3implementation; In Progress → In Review93e57a0, Spec → Readyd4f7401, Backlog → Specdc18b15): Bind the parent-inputVALUEScells of split/reference DataLoader rows methods through the key column’s jOOQ ConverterDataType. The rows methods built each parent-key cell straight from the raw keyField(typed byColumnRef.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_domainover BIGINT with aConverter<Long, String>) bound ascharacter varyingand every@splitQuery/@referencechild nulled out withoperator 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(@tableMethodvariant),buildServiceTableLift(R285 lift-back re-projection), andMultiTablePolymorphicEmitter.buildParentInputValuesEmitter+ per-branch ON lookups, now route their cells throughValuesJoinRowBuilder.cellsCode(extended with a constants-class-table-expression variant, making its single-VALUES-cell-authority javadoc claim true), emittingDSL.val(<scalar>, Tables.<OWNER>.<COL>.getDataType())so jOOQ binds through the registered Converter at the DB type (coerceandcastwere verified against jOOQ 3.20.11 and rejected in the Spec). The scalar extraction forks onSourceKey.Wrap, the axis that actually decides value accessors (replacing the coupledreader() instanceof AccessorCallfork):RecordNkeys readk.valueN();RowNkeys recover the value from the bindParamvia a new per-fetcher-classparentKeyCellValuehelper with a loud statement-body throw, the documented contract for@sourceRowlifter 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 (FkJoinorigin /LiftedHoptarget /ConditionJoinparent); the polymorphic arm’s owner (the parent/hub table) ridesInterfaceField/UnionFieldas a non-nullparentKeyOwnerTablecomponent threaded from the resolution sites. JOIN-predicateparentInput.field(...)lookups switch to the owner column’sDataTypefor symmetric type metadata;RowN/RecordNgeneric type-args staycolumnClass()-typed (the converter’s user type IS the Java-side type). Fixtures pin end-to-end:org_code_domain(BIGINT) +converter_org/converter_campusin the fixture DB (schema version 2.3→2.4),OrgCodeStringConverteringraphitron-fixtures-codegenregistered via<forcedTypes>, and sakila-example execution tests covering single-cardinality@splitQuery(the reportedCampus.organisasjonshape), list-cardinality@splitQuery, and a@sourceRowlifter over the converter-backed key (the liveparentKeyCellValueParam-contract pin); existing unit expectations updated in place, no new body-substring assertions. Independent-session In Review → Done review; full reactor green undermvn clean install -Plocal-db(2419 core tests + sakila-example compilation/execution tiers,GraphQLQueryTest297 tests, 0 failures) after re-seeding a stale localrewrite_testDB (missing the R389partyfixtures and R413’s ownconverter_*tables), the sameQuery.allParties/JoinedTableInheritancePipelineTeststale-DB artifact every recent Done review documents (standing fix: R418), on paths R413 does not touch. -
R414 (
f106d52implementation; In Progress → In Reviewae9226b, Spec → Readyc0b4ff7, Backlog → Spec6ea032b): Serve a real per-parenttotalCounton split/DataLoader-backed connections, closing the last classification path that produced null-(table, condition)ConnectionResultcarriers on reachable queries. A nested (non-root)@splitQueryconnection advertisedtotalCount: Intin the emitted SDL but always resolved it tonull: the scatter path (SplitRowsMethodEmitter.scatterConnectionByIdx) built each per-parentConnectionResultthrough the(result, page)convenience constructor, which passednullfor the(table, condition)pair the generatedConnectionHelper.totalCountneeds to issue itsSELECT count(*). The fix mirrors the B4c-2 polymorphic-batched semantics (MultiTablePolymorphicEmitter.buildBatchedConnectionRowsMethod): the rows method hoists its WHERE into a singleCondition wherelocal (buildWhereConditioncalled exactly once, since it declares FK-target alias locals as a side effect) shared by the windowed page query and a new cursor-independentcountSourcederived table (same join topology viaemitFromBridgeAndParentJoin, no orderBy/seek so the count is window-independent);scatterConnectionByIdxgains aTable<?> countSourceparameter and binds each per-parent carrier via the 4-argConnectionResultconstructor withcountSource.field("__idx__", Integer.class).eq(DSL.inline(i)), soConnectionHelper.totalCountrunsSELECT count(*) FROM countSource WHERE __idx__ = ilazily on selection (zero count SQL when unselected, N counts for a batch of N parents when selected). The now-dead two-arg(result, page)ConnectionResultconstructor is removed (scatterConnectionByIdxwas its only caller; this is generated-into-consumer source, not published API); the nullable(table, condition)field shape and theif (cr.table() == null || cr.condition() == null) return nullguard 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’sActorsConnection(shared byFilm.actorsConnection+Film.actorsOrderedConnection, both@splitQuery) gainstotalCount: 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 oneemitFromBridgeAndParentJoinhelper and the single hoistedwhere, so drift risk is low (possible follow-up if it ever drifts). Coverage: execution-tierGraphQLQueryTest(splitQueryConnection_totalCount_isParentScopedcounts 2/2/1/1/1 for films 1-5 against the seededfilm_actorrows withfirst: 1to distinguish the count from the page size;_isCursorIndependentstill reports 2 paging past anaftercursor;_isLazyOnSelectionasserts noselect countSQL when unselected and exactly N per-parent count statements when selected) and pipeline-tierSplitTableFieldPipelineTest(structural:scatterConnectionByIdxcarries theTable<?> countSourceparameter), no code-string assertions on generated method bodies. Mirrors B4c-2’s count semantics; per-parenttotalCountfor hypothetical future split shapes (SplitLookupTableFieldhas no connection arm today) stays out of scope. Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db(2418 core tests + the sakila-example compilation/execution tiers, 0 failures). Thegraphitron-core red first seen in the review sandbox (Query.allParties→UnclassifiedField;JoinedTableInheritancePipelineTestClassCast on the R389party/jti_*fixtures) was the known stale-rewrite_test-DB artifact every recent Done review documents, the local DB predating the R389 fixtures and jOOQ’sschemaVersionProvidersuppressing catalog regeneration; re-seeding frominit.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 (
60b58aephase 0 plumbing,6021bdaphase aJooqConvert,fb1b2e4phase bNodeIdDecodeKeys,feadbcbphase c developer@condition,24b2527In Progress → In Review; Spec → Ready re-reviewbd2e761): Lift the remaining filter-argument kinds onto multitable interface/union root query fields, completing the arc R363 (branch-safeDirect/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 widensMultiTablePolymorphicEmitter’s root entry points to carry the enclosing `<Type>Fetchersclass’sCompositeDecodeHelperRegistryand 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.declareAliasesper participant, namespaced by thestage1_<Type>base), deduped<name>Keyslocals forJooqConvert-list args, and shared lifted-outerMaplocals, replacingbranchFilterWhere’s former `emitTerm(..., null, null, Map.of())with the threaded values (behaviorally inert, proving the seam before any arm flips). Phase a flips theJooqConvertarm branch-safe and, per R267 (fix a deprecation-for-removal at the source, never suppress), replaces the deprecated-for-removalDataType.convert(Object)inArgCallEmitter’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@fieldleaf with top-level conversion semantics (an ID-typed nested@fieldover a plain column now carries aJooqConvertleaf instead of the hardcodedDirect) and carvesJooqConvertleaves out ofCallParam.emitsUncheckedCast(theinstanceof List<?>guard casts nothing). Phase b flipsNodeIdDecodeKeysbranch-safe, homing the drained decode helpers on the<Type>Fetchersclass 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-classcollectIntobracket is the true single-table precedent). Phase c removes the R363 field/arg-levelhasConditionpre-guard and relaxesfirstUnsupportedFilterArg’s first guard so developer `@conditionfilters (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, aTable<?>first parameter serving every branch while a concrete-table parameter surfaces a mismatch at consumer javac (R379 semantics). The exhaustive nine-permitisBranchSafeExtractionswitch stays the forcing function:NodeIdDecodeRecord/InputBean/JooqRecordremain explicitfalsearms (mutation-input/record-decode shapes that do not occur as a multitable root-query filter arg). Coverage: pipeline-tierMultiTableFilterLoweringTest(each kind’s rejection test flips to a lowered per-participant assertion on the model,JooqConvert/NodeIdDecodeKeysextraction, nested-leaf alignment, three@conditioncases including nested-input) and execution-tierMultiTableFilterExecutionTestover theAddressOccupant = Customer | Staffunion (occupantsByStoreIdper-branch coercion,occupantsByAddressdecode-and-filter plus a wrong-type-id client-error,occupantsStartingWithM/occupantsByNamePrefix/OccupantFilter.namePrefixvia the newTable<?>-genericMultiTableConditionFixtures), withgraphitron-sakila-example’s `-Xlint:all -Werrorcompile 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 undermvn clean install -Plocal-db(0 failures) after re-seeding a stale localrewrite_testDB (missing the R389partyfixture) and regenerating the jOOQ catalog, the sameQuery.allParties/JoinedTableInheritancePipelineTeststale-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 → Ready197307d, Backlog → Spec51503c5): Clamp connectionfirst/lastat the single runtime choke point and unify the no-channel error disposition. A negativefirst/laston a connection field flowed unvalidated into the SQLLIMIT, so PostgreSQL threwLIMIT must not be negative, which the framework redacted into an opaque correlation-id 500 instead of a client-facing validation error;first: 2147483647produced the identical redacted 500 vialimit = pageSize + 1wrapping toInteger.MIN_VALUE. Fixed in the emittedConnectionHelper.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 markerGraphitronClientExceptionso the real message reaches the client, negativefirst, negativelast, and the derived-limit overflow (pageSize == Integer.MAX_VALUE, guarding the value PostgreSQL actually enforces rather than each input, which also covers a pathologicaldefaultPageSize); the pre-existing mutual-exclusionIllegalArgumentException("first and last must not both be specified")migrated onto the same marker (same client-mistake family, same redaction defect).first: 0stays valid. The purity note reworded to the load-bearing property (pageRequesttakes noDataFetchingEnvironment; the marker subclassesGraphqlErrorException, so "no graphql-java dependency" was never the true invariant) at bothConnectionHelperClassGeneratorandTypeFetcherGenerator: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 tosurfaceClientErrorOrRedactwhile leaving the two async.exceptionallyarms on plainredact, so a client error on a nested (DataLoader-based)@splitQueryconnection would still redact. Lifted the router call into one shared definitionErrorRouterClassGenerator.noChannelRouterCall(outputPackage, throwableVar)consulted by all no-channel sites, bothnoChannelCatchArm`s, both `asyncWrapTailno-channel branches, andChannelCatchArmEmitter(a fifth site already on the right disposition); the two async arms flip tosurfaceClientErrorOrRedact, whose cause-chain walk unwraps theCompletionExceptionDataLoader 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-tierGraphQLQueryTest(filmsConnectionnegativefirst/lastsurface the argument-naming message and never containAn error occurred. Reference:,first: 2147483647surfaces the overflow message,first: 0returns an empty page with a computablehasNextPage, the migrated collision message, and the load-bearingfilmById(...) { actorsConnection(first: -1) }proving the async-arm flip end-to-end through the DataLoaderCompletionExceptionunwrap, which a root-connection test alone would pass without the flip), unit-tierErrorRouterClassGeneratorTest.noChannelRouterCall_emitsSurfaceClientErrorOrRedact, and updatedTypeFetcherGeneratorTestarm pins. Out of scope, filed nowhere yet: malformedafter/beforecursor 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 undermvn install -Plocal-db(GraphQLQueryTest294 tests, 0 failures) after re-seeding a stale localrewrite_testDB (missing the R389party+jti_*joined-table fixtures) and a clean jOOQ catalog regen, the sameQuery.allParties/JoinedTableInheritancePipelineTeststale-DB artifact the R416/R407/R182 Done reviews documented, on paths R415 does not touch. -
R416 (
71f5429In Progress → In Review implementation;d58ae37Dependabot #504/#506 closure record;d893673self-review charset-tolerant asset content-type assertions; Spec → Ready943884f, Backlog → Spec9ad3ff1, Backlogc3623f2): Self-host the GraphiQL playground assets ingraphitron-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 withvite@6.4.2/@vitejs/plugin-react@4.7.0) ships as plain classpath resources underno/sikt/graphitron/jakarta/rest/graphiql/and is streamed by a new path-traversal-safe@GET assets/{name}method onGraphqlResource(a[A-Za-z0-9._-]+allowlist + explicit..reject + extension→MIME gate overjs/css/map/ttf/woff/woff2/svg, all behind the existinggraphiqlEnabled()seam). Vendor-neutral by design:getResourceAsStream, notMETA-INF/resources/(which only serves on Quarkus, contradicting the module’s Jakarta-EE-neutral ethos).graphiql()gained@Context UriInfoand 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’sbase: './'. The Vite recipe wasgit mv’d from the (now GraphiQL-free) `graphitron-sakila-exampleintographitron-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.ttfand fonts; the broader R399 app-section README drift (deadGraphqlEngine/GraphqlResource/AppContextlinks) split out as follow-up R417 rather than expanded into scope. The companion/opt-in-graphiqlartifact (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 (thegraphiql.htmlrationale 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@Testclasses):graphitron-sakila-example’s `GraphqlResourceSmokeTestgained six page + asset conformance checks (self-hosted page has the mount div, nounpkg.com, and a resolved.../graphql/assets/base; the entrygraphiql.js/graphiql.cssstream with the right content-type; missing and unknown-extension names 404). Independent-session In Review → Done review; full reactor green undermvn clean install -Plocal-db(2416 core tests + the sakila-example compilation/execution tiers,GraphqlResourceSmokeTest6/6, 0 failures). Thegraphitron-core red the implementer reported as "pre-existing trunk breakage" (Query.allParties→UnclassifiedField,JoinedTableInheritancePipelineTest, and thePerson/AppAccountjoined-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 R389party+jti_*fixtures so its jOOQ catalog lacked the detail tables' primary keys; re-seeding frominit.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 (
406f80dimplementation; In Progress → In Review2f5f741, Ready → In Progressd910a1f, Spec → Ready825b524, build-command fixupb483e1e): Exclude generator-injected federation/@linkdefinitions from the R398 SDL lint engine. A consumer schema carrying a federation@linksawtype-names-pascal-caseandtypes-and-fields-have-descriptionswarnings onfederation__FieldSet,link__Import, and siblings the author never wrote and cannot rename (names dictated by the federation spec) or document (descriptions owned byfederation-graphql-java-support); these definitions carry anullsource, the tell that they came from no consumer.graphqls. Fixed by provenance, not aname.contains("__")heuristic and not by borrowingScalarTypeResolver.FEDERATION_NAMESPACE_SCALARS(a hand-maintained expectation free to drift on a spec bump):FederationLinkApplier.applynow returns theSet<String>of names it injected (collected in its existingdefs.forEachloop, the sole contributor) instead of a bare boolean.AttributedRegistrycarries that set asinjectedNames()and derivesfederationLink()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.rungains an overload taking the set and unions it with the existingBUNDLED_TYPE_NAMESexclusion 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 newLintRule, no newLintNodeKind.KeyNodeSynthesiseris untouched: it decorates author@nodetypes in place with@keyand injects no new definitions, so folding its names in would wrongly silence real author violations; it keeps itsvoidsignature. Coverage: pipeline-tierLintInjectedFederationDefinitionsTestcarries a federation@linkplus an authortype 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 typedLintRuleand the minimum node-identity check, no rendered-message or generated-body assertions;FederationLinkApplierTestupdated to assert on the returned name set. No per-name unit list. Reuses R398’sBUNDLED_TYPE_NAMESmechanism; sibling to R408 (author-driven suppression, the "chooses not to fix" half). Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db(2407 core tests + sakila-example compilation/execution tiers, 0 failures) after re-seeding a stale localrewrite_testDB (missing the R389party+jti_*joined-table fixtures) and regenerating the jOOQ catalog, the sameQuery.allParties/JoinedTableInheritancePipelineTeststale-DB artifact earlier Done reviews noted, on paths R407 does not touch. -
R182 (In Review → Done; landing sequence
657dd71delete legacy reactor,a505cabunwrap rewrite to repo root,aad2f61restructure architecture docs into Diataxisdocs/architecture/,cbaeb64CI/CD workflows,31750bfdocs+tooling paths; In Review transitionb0cb778; In Progresse90e321; R19 discardadb071a): Retired the legacygraphitron-parentreactor and unnestedgraphitron-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 rootpom.xml+maven-build.ymlare gone; the rewrite aggregator (graphitron-rewrite-parent) is now the root POM with its eleven modules +docsat the top level, and the duplicategraphitron-javapoetcollapses to one copy. This closes therelease-event publish hazard:maven-publish.ymlonmaindrops the-f graphitron-rewrite/pom.xmlflag 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 flatgraphitron-rewrite/docs/tree became a Diataxis-shapeddocs/architecture/{explanation,reference,how-to}/folded into the site module (README.adocsplit intoindex.adoc+reference/modules.adoc+explanation/pipeline-overview.adoc;getting-started.adocdissolved into the manual how-to pages plus a newhow-to/dev-loop-internals.adoccarrying the#dev-loop-detail/#native-runtime-dependencyanchors), and roadmap-internalworkflow.adocmoved out of the site toroadmap/workflow.adoc.roadmap-tool’s `Main.javagained a quadrant-awaremapAdocTarget(driven by anARCH_QUADRANTslug table) plus repointed README/status-board headers; the ~11 inbounddocs/**architecture xrefs were repointed; CI path prefixes and thetree-sitter-natives-release.ymlenv vars were de-prefixed.verify-standalone-build.shretired, the CLAUDE.md legacy-scope rule removed, andrewrite-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 undermvn install -Plocal-db(2406 tests ingraphitron, docs site renders the Diataxis/architecture/tree withworkflow.adoccorrectly absent). Thegraphitron-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 R389partyfixture and jOOQ skipped codegen against it); re-seeding frominit.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 pass1b6971d, In Review → rework1f4412f, Spec → Ready6296d0b, spec5f5ebc2, filed1633934): Support a single-table discriminated interface (@table @discriminate, implementers pinned by@discriminator(value:), all sharing one jOOQ table, e.g.Contentovercontent) as a DML@mutation(typeName: INSERT|UPDATE)return type. Before this, such a return was not rejected but silently mis-accepted (aTableInterfaceTypeis aTableBackedType, so it classified through theTableBoundReturnTypearm toProjectedSingle/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-@tablewrite (the discriminator is an ordinary@field(name: "CONTENT_TYPE")input column the client sets;resolveInputunchanged); the entire fix is on the return half. Model: added the siblingDmlReturnExpression.DiscriminatedSingle/DiscriminatedListarms carrying the read-side discrimination data (interfaceName,discriminatorColumn,knownDiscriminatorValues,TableBoundparticipants) sourced verbatim from theTableInterfaceTypeverdict, the DML sibling of R405’s*ServiceTableInterfaceField; both map to the sameRecord/Tabledomain-return + target shape asProjected*. A new return-shape arm (not a per-verbMutationFieldleaf) 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 chokepointbuildDmlFieldresolves the return’s look-ahead verdict once and threads it into the still-staticbuildDmlReturnExpression, which builds theDiscriminated*arm when the verdict is aTableInterfaceType. Validate:dispatchPerformsReFetchrecognises theDiscriminated*arms as re-fetching, keeping the emitter andOutputField.requiresReFetch()in lockstep under the build-time drift guard; the DELETE and@asConnectionfloors already fire for the interface case through the sharedTableBoundReturnTypearm. Emit: consumes R405’s shared read-side re-projectionTypeFetcherGenerator.buildTableInterfaceReprojection(passingList.of()foralwaysProject, since the DML path keys the follow-up SELECT by a PK-INConditionoff theRETURNINGkeys rather than re-mapping by PK like the service path); the duplicatebuildDiscriminatedReprojectionfrom the first pass is deleted. Step 1 (PK-onlyRETURNINGindsl.transactionResult) and the composite-safe PK-IN builder were extracted intoemitKeysTransaction+buildPkKeysCondition, R406-owned and shared only betweenemitProjectedandemitDiscriminated(the DML write half has no R405 equivalent; they key off theRETURNINGkeyslocal, distinct from R405’srecords-keyedMultiTablePolymorphicEmitter.buildPkInCondition). The generated row carries__discriminator__; the interface’s existingTypeResolversets__typenameper 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 returnsnull, since the discriminator filter cannot name a subtype outside the known set; enforcing the discriminator domain is the database’s job (aCHECKconstraint), 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-tierGraphitronSchemaBuilderTest(DiscriminatedSingleINSERT + UPDATE +DiscriminatedListclassification, a regression pin against the pre-R406ProjectedSinglesilent-accept, and a DELETE-floor rejection pin), pipeline-tierFetcherPipelineTestINSERT/UPDATE shape assertions, and execution-tierDmlTableInterfaceReturnExecutionTest(real PostgreSQL over thecontentfixture + a newContentInput/createContent/updateContentschema fixture: per-__typenamerouting off the live discriminator, the cross-tableFilmContent.ratingjoin, same-tableShortContent.descriptionisolation, and the unknown-discriminator write/read asymmetry). Independent-session In Review → Done review; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. The full-reactorgraphitron-core red first observed in the review sandbox (Query.allParties→UnclassifiedField;JoinedTableInheritancePipelineTestClassCast on the R389jti_*fixtures) was the known stale-rewrite_test-DB artifact (missingjti_app_account/jti_personcomposite PKs) compounded by jOOQ’s up-to-date codegen skip; re-seeding frominit.sql+ a clean catalog regen produced a fully green reactor including R406’s execution tier, and the failures are orthogonal to R406. Consumes R405’sbuildTableInterfaceReprojection(extracted read-side, landed7b9d051). Non-blocking carry noted at review: the twoFetcherPipelineTestcases useCodeBlock.toString().contains(...)body-string assertions (existing DML-fetcher precedent in the same file; model shape pinned inGraphitronSchemaBuilderTest, behaviour at the execution tier). -
R370 (core four sites + fixtures + R412 filing
a13523e; two remaining in-hand@service-path sites + witnesses4e22ee4; Spec → Ready7a853a5, In Review → Ready rework352cc16): A record-backed parent with a nested backing class emitted the non-compiling$-qualifiedOuter$NestedbecauseClassName.bestGuessre-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-oneClassName.get(Class)/ captured-TypeNamesubstitution at the source boundary rather than a per-consumer patch: the twoAccessorRefproducers (deriveAccessorRecordParentSource,derivePolymorphicHubSource), the@servicereturn-type validator (checkServiceReturnMatchesPayload, previously spuriously rejecting nested payloads at classify time), and the query + mutation@servicefetcher return types (computeServiceRecordReturnType,computeMutationServiceRecordReturnType, both collapsed toServiceMethodCall.javaReturnType()so the twins no longer drift), plus the@serviceOutcome payload ctor arm (resolveErrorChannel→ClassName.get(payloadCls)). RestoresAccessorRef’s own javadoc contract and corrects the now-true "Identical policy" mirror javadoc; the stale R370 hazard note in `buildScalarPerParentFetcherwas removed. Coverage: two compilation-tier fixtures, one perAccessorRefproducer (NestedFilmsPayloadlist-arm viabuildAccessorKeyMany,NestedOccupantCarriersingle-cardinality polymorphic viabuildScalarPerParentFetcher); a mutation +@error-channel compilation fixture (NestedFilmReviewPayload) reachingcomputeMutationServiceRecordReturnTypeandresolveErrorChannel; and a classification-tierErrorChannelClassificationTest.childServiceRecordField_nestedPayloadBacking_payloadClassIsStructurallyResolvedpinning the resolvedPayloadClass.payloadClass()to the structuralOuter.Nestedby object-equality on theTypeName(no code-string assertion on any generated body). The review corrected the spec’s second-witness premise: after the R244 Outcome flip, root@serviceoutcome fields classify toErrorChannel.Mapped(no developer payload class emitted), soresolveErrorChannel’s `PayloadClassarm is reached only by a child@servicefield, hence the classification-tier witness rather than a sakila compile fixture. The remainingbestGuess-over-fqClassNameemit sites that hold no reflectedClass/capturedTypeName(backingClassOf,recordColumnReadArgs,FetcherEmitter, severalChildFieldsites) need a model-lift, not a call swap, and were filed as R412. Independent-session In Review → Done review; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(2406 core tests + the sakila-example compilation/execution tiers, 0 failures) after re-seeding a locally-clobberedrewrite_testcatalog (theQuery.allParties/JoinedTableInheritancePipelineTestcascade was a stale-DB artifact orthogonal to R370). -
R405 (landing
7b9d051; Spec → Readye133e55, Backlog → Specca05952, flow/mechanism capturef61566b, filed1633934): Support a single-table discriminated interface (@table @discriminate, implementers pinned by@discriminator(value:), all sharing one jOOQ table) as a root@servicepolymorphic return, closing the last deferred shape on the@servicepolymorphic 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. NewQueryServiceTableInterfaceField/MutationServiceTableInterfaceFieldleaves (single-table siblings of*ServicePolymorphicField) carry the shared-@tableTableBoundReturnType, discriminator column + known values, andTableBoundparticipants plus the service binding; target shapeInterfacekeepsrequiresReFetch()false. Both root classifiers'TableBoundarms build the new variant when the verdict is aTableInterfaceType(deferServiceTableInterfacedeleted). Emit reuses the read-side projection: the shared discriminator-filter +__discriminator__projection + cross-tableLEFT JOINassembly was extracted from the two read fetchers into a package-privateTypeFetcherGenerator.buildTableInterfaceReprojection(read paths pass an emptyalwaysProject, output unchanged); the newMultiTablePolymorphicEmitterservice fetcher calls the service, normalises the return toList<Record>(extracted shared snippet), collects the shared table’s PKs into a composite-safeDSL.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, yieldsnullfor a single). Validate mirrors the single-table floor (validateCardinalityonly, not the multi-table participant check). Union returns stay permanently unsupported; child@servicepolymorphic returns and@asConnectionstay out of scope. Coverage: pipeline-tierServiceTableInterfaceReturnPipelineTest, unit-tier classification flip (serviceReturningTableInterface_classifiesAsServiceTableInterfaceField+ mutation twin) andServiceTableInterfaceFieldValidationTest, the twoClassifiedCorpusexamples +GeneratorCoverageTestleaf-partition pin, and execution-tierServiceTableInterfaceReturnExecutionTest(real PostgreSQL: list routes each row toFilmContent/ShortContentoff the live discriminator, populatesFilmContent.ratingvia 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-reactorgraphitron-core red observed in the review sandbox (Query.allParties→UnclassifiedField;JoinedTableInheritancePipelineTestClassCast) was a stale-rewrite_test-DB artifact (missingparty_*tables +jti_personPK) compounded by jOOQschemaVersionProviderregeneration caching, reproduced identically at the pre-R405 parent8fca064and orthogonal to R405; re-seeding the local DB frominit.sql+ a clean rebuild produced a fully green reactor including R405’s execution tier. Non-blocking follow-up noted at review: the pipeline test usesCodeBlock.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 (
cb99991implementation;8fca064fixture-gate accounting; Spec → Ready6ad75dd): Deprecation signal for@tableon 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@tabledescription indirectives.graphqlsgains the input-type deprecation note + replacement instruction (no SDL@deprecatedmarker, the spec forbids it on a directive location); a directive-level row indeprecations.adocwith the section heading/intro widened from "whole directives" to "whole directives and directive locations"; a WARNING admonition on the canonicaltable.adocpage; and thecode-generation-triggers.adocInput type with @tablerow annotated. Actionable tier (fires per@table-on-input type, respects the carve-out): a post-classificationGraphitronSchemaBuilder.emitTableOnInputDeprecationWarnings(ctx)pass placed besiderejectCaseInsensitiveTypeCollisions, walkingctx.schemainput types that explicitly declare@tableand emitting a non-fatalBuildWarning.NoRuleper usage (the plain non-lint arm per D1, notLintFinding; message names noR<n>per D2; unconditional per D4). The encoded-ID / scalar-return INSERT/UPSERT carve-out (D3) is computed byencodedWriteTargetInputTypes(ctx)off the classified model (theMutationInsertTableField/MutationUpsertTableFieldleaves whosereturnExpression()is anEncoded*arm), needing nolookAheadVerdictor reflection, so R332 staysdepends-on: []; the type-level conservative rule suppresses an input reused by any encoded INSERT/UPSERT.encodedWriteTargetInputTypesis the named find-usages anchor R97 Phase 2b retires (forward edge R97 → R332). Coverage: pipeline-tierTableOnInputDeprecationWarningTest(projected-return INSERT warns with source location; encoded-ID INSERT carved out; D3 reuse suppression) 3/3 green;DeprecationsDocCoverageTestgains"table"inWHOLE_DIRECTIVE_DEPRECATIONS;FixtureWarningsGateTestsegregates the R332 category out of its exactly-one advisory scope (mirroring the ENGINE-lint filter) and adds a count-independent carve-out test (FilmCreateInput/FilmDeleteInputwarn, encoded-IDCreateKeyedNodeInputdoes not); no code-string assertions on generated bodies. Independent-session In Review → Done review;TableOnInputDeprecationWarningTestgreen and the emittedNoRulemessage shape matches both gate matchers by inspection. The twographitron-sakila-exampletests could not execute because that module’sgenerategoal is blocked on trunk by the pre-existing R389 fixture breakage (party/party_individual/jti_personcatalog tables absent,Query.allPartiesunclassified); the 11graphitron-core failures (JoinedTableInheritancePipelineTestClassCast + theQuery.allPartiescorpus 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 (
1d520faengine + nine visitors,82fd246sealedBuildWarning+ advisory tagging + MCP projection,148c8acLSP finding-keyed QuickFix,fd38fb0@recorddeprecation-marker alignment,53d475breport/LSP integration pins,d5b0a57In Progress → In Review,3a4a190explicit-fix decoupling; Spec → Ready3ee7062): 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 theLintVisitor`s subscribed to its `LintNodeKind; adding a rule is registering it inLintRules, not editing a central switch. Rule identity is a type (LintRuleenum, stable kebab-caseid(), aSource{ENGINE, CLASSIFIER}axis), never a string bag. Findings ride the existing warning channel via a sealedBuildWarning(NoRulearm for the pre-existing untagged advisories;LintFindingarm carrying the typedLintRule+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 intoValidationReportunchanged, so the LSP replay (Diagnostics.validatorDiagnostics, R139 freshness-silence intact) and the MCPdiagnosticstool project findings with no second evaluator, the MCP wire additionally carrying theLintRuleid. 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 theirFieldBuilder/TypeBuilderemit sites, never re-derived, so the classifier stays their sole emitter and each coordinate is warned exactly once (no-deprecated-directive-usageexcludes@record, owned by the redundant-record advisory). OptionalLintFixis a suggestion the LSP turns into aQuickFixCodeAction(a new finding-keyed branch alongside the detector-drivenSdlActionspath, sharing only theWorkspaceEdit/TextEditemit 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-javaDeprecationRecognizer(the@deprecateddocstring-token regex + native-marker read + typedDeprecationInfo{NATIVE, DOCSTRING}) is extracted down fromgraphitron-lspinto thegraphitronbuild module so visitor 8 can consume it build-side;LspVocabularynow delegates and keeps itsSchemaCoordinateadapter LSP-side (zero behaviour change, pinned byLspVocabularyTest/SdlActionDriftTest), and@recordgains the docstring@deprecatedmarker so the convention is uniform. Coverage: pipeline-tierLintEngineTest(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 partitionsLintNodeKindwith no overlap/gap, mirroringVariantCoverageTest/EdgeCoverageTest),ClassifierAdvisoryFixPipelineTest(emit-site fixes + bare-only guard), LSP-tierLintQuickFixTest+ValidatorDiagnosticsTest(build-side finding replays into aWarningsquiggle at its range and applies to the corrected SDL, silenced on a stale snapshot per R139), and MCP-tierGraphitronMcpServerTest(both arms: no-rule advisory carries nolintRule, lint finding carries its id on the wire); findings asserted on the typedLintRule+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@splitQueryon@record) and R296 (deprecated-directive usage). Independent-session In Review → Done review; the full reactor is green under-Plocal-dbonce the localrewrite_testDB is seeded from the currentinit.sqland jOOQ regenerated (a stale sandbox catalog missing the R389partyjoined-table fixtures reproduces the sameQuery.allParties/JoinedTableInheritancePipelineTestclassification failures earlier Done reviews noted, on paths R398 does not touch). Approval corrected one false-invariant javadoc onLintNodeKind(it claimed a throw-on-unmapped instanceof chain the engine does not implement; the engine names the kind explicitly per dispatch site). -
R409 (
d2c8363implementation,9a22265In Progress → In Review; Spec → Ready972aeaf): Quiet the non-actionablemvn graphitron:devRAG-warm startup log noise and document the recommended consumer.mvn/jvm.configfor the warnings a plugin cannot un-print. A newRagLogQuietinghelper lives ingraphitron-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 fromDevMojo.bindServerbefore the warmsstart()on the dev thread, so thread-start’s happens-before edge publishes the suppression to thegraphitron-warm-*daemon threads that load the noisy classes (in-code comment forbids reordering it afterstart()). Group 1 (DJL HuggingFace tokenizermaxLengthwarning): 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 FQCNai.djl.huggingface.tokenizers.HuggingFaceTokenizerconfirmed against the1.16.3-beta26bge jar. Group 2 (LuceneVectorizationProviderincubator-module warning): actionable and directional, so demote-do-not-swallow, its JUL logger raised to SEVERE and, only whenjdk.incubator.vectoris absent (ModuleLayer.boot().findModule(...)), one concise graphitron-owned dev line names the--add-modules jdk.incubator.vectorflag in place of Lucene’s multi-line dump; present module → silent (fast path already on). Group 3 (Maven-runtime jansi native-access / guavaUnsafewarnings): 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 ownlib/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 withGenerateMojo/ValidateMojo, not triggered by theGraphitronMcpServerconstructor, not to be hoisted). Coverage: unit-tierRagLogQuietingTest(Lucene JUL SEVERE; DJL slf4j-simple propertyerror+ 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’sgraphitron-mcp+graphitron-maven-pluginmodules compile clean andRagLogQuietingTestis green, and the full-reactorgraphitron-core failures (Query.allParties→UnclassifiedField;JoinedTableInheritancePipelineTestClassCast) were confirmed to reproduce identically at the pre-R409 parent commit5027e30, on classification paths R409 does not touch. -
R261 (landing
6d5ce8b, Spec → Ready7af72bf): Generation-time wire-coercion cast guard, Slice 1 (the three@servicearg-classification sites). Before this, every arg-classification site fell through toCallSiteExtraction.Directand emitted a raw(DeclaredType) wireValuecast that compiled cleanly andClassCastException`d (or, for enums, `IllegalArgumentException`d) on the first request, since graphql-java delivers `ID/enum asString,IntasInteger,FloatasDouble, input-objects asMap. The fix homes the "coercion output assignable to declared type" verdict at the classifier in a newWireCoercionResolver(a sealedPassThrough | Rejectedresult, mirroringEnumMappingResolver.EnumValidation), consuming a new pure forward mappingScalarTypeResolver.coercionOutputType(SDL scalar name → coercion-outputTypeName, over spec built-ins, federation scalars, and classified@scalarTyperesolutions), keeping the verdict offScalarTypeResolver(D1). A newWireCoercionErrorsub-seal ofRejection.AuthorErrorcarries two arms on two axes (D4):Assignability(coercion class ≠declared type; sites A-D) andEnumConstantDivergence(declared type is the enum but an SDL value name has no matching Java constant; site E), each with a stablelspCode()undergraphitron.wire-coercion.*wired intoDiagnostics.lspCodeOf,RejectionSeverityCoverageTest, andtyped-rejection.adoc(+ drift list).ServiceCatalog.argExtraction(site B) is widened to take the resolved SDL leaf and return a sealedResolved | 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 throughEnumMappingResolver.checkEnumConstants, an extracted column-agnostic single parity home reused byvalidateEnumFilter(column path) and the@serviceenum producers (D3). Coverage:WireCoercionCastGuardPipelineTestasserts per arm on the typed rejection’slspCode()/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 wholegraphitron-lspmodule clean. Sites C (@condition) and D (@externalField) were carved into R411 (reject-wire-coercion-nonservice-sites,depends-ondimensional-model-pivot), which consumes this predicate unchanged; R261 droppeddimensional-model-pivotfromdepends-onand closes on the@serviceslice alone, with the@tableMethod/@conditioncaller deliberately left onServiceCatalog.legacyArgExtractionuntil R411 threads the predicate through R222’s channel. The sandbox’s pre-existingQuery.allParties/JoinedTableInheritancePipelineTestfailures (missing PK metadata on thejti_*fixture catalog) were confirmed to fail identically at the pre-R261 parent commit6ad75dd, 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 → Ready9ea2a8c): Lift the DML UPSERT/UPDATE dialect requirement off hand-builtpostDslGuardCodeBlock`s onto typed model data. A new sealed `DialectRequirement(None/RequiresFamily/RejectsFamily) plus a graphitron-internalSqlDialectFamilyenum (a jOOQSQLDialect.family()collapse; name-prefixfromDialectNamecovers the commercial-onlyORACLE*/POSTGRESPLUSspellings the OSS jOOQ distribution omits) make the "UPSERT rejects Oracle" / "bulk UPDATE requires Postgres" facts discoverable onMutationField.DmlTableField.dialectRequirement()(never null), so the verb-neutralbuildDmlFetcherskeleton 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 itsFieldBuilderconstruction site (UPSERT →RejectsFamily(ORACLE), bulk UPDATE →RequiresFamily(POSTGRES)selected oninputArg.list(), INSERT/DELETE/single-row UPDATE →None.INSTANCE);MappingsConstantNameDedupthreads it through the error-channel rebuild.buildDmlFetcher’s `postDslGuardCodeBlockparam becomes aDialectRequirement, collapsing three overloads into two, and a newemitDialectGuardhelper 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-internalSqlDialectFamilyenum into generated code, which does not compile in a consumer, wheregraphitronis test-scoped (graphitron-sakila-example/pom.xml) while the generated fetchers compile as the consumer’s main sources (GenerateMojoadds them viaproject.addCompileSourceRoot); the emitted guard instead stays self-contained, comparing jOOQ’s owndsl.dialect().family().name()against the family’sjooqFamilyName()(the reachable bulk-UPDATE output is byte-identical to the former inlinefamily().name().equals("POSTGRES")guard, and jOOQ’sfamily()folds everyORACLE*spelling toORACLE, so the UPSERT gate still catches them).fromDialectNameis retained on the model for the future validator-time check.postInGuard, the sibling free-formCodeBlockcarrying imperative per-row emission mechanics, stays aCodeBlock(documented non-goal: noDialectRequirement-shaped datum hides in it). Coverage: unit-tierSqlDialectFamilyTest(fromDialectNamecollapse +jooqFamilyNameincl. theOTHERrejection), pipeline-tierDmlDialectRequirementClassificationTest(per-verb population), emitterTypeFetcherGeneratorTest(self-contained Oracle guard from a directly-constructed field +Noneemits nothing; UPSERT can’t classify through the pipeline under R144), and the bulk-UPDATEFetcherPipelineTestassertion 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-existingQuery.allParties/JoinedTableInheritancePipelineTestfailures (missing PK metadata on thejti_*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-transformintographitron-rewriteshipped its build surface (schema loading, tagged inputs, Maven plugin, aggregator-standalone, content-idempotent writes) and@asConnectionemit-time synthesis; the Java LSP rewrite +devgoal landed under R18;@notGenerateddirective 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, deletinggraphitron-maven-pluginwholesale 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 deletion27412d3+ xref strip2ea7900; In Review4771bcd; rework fix76d5ac9; spec/AC notescff3120/2eca156; In Review160ecef): 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(theroadmap-toolthat generatesdocs/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, andWITHHELD_FROM_V1= {tableMethod,sourceRow,experimental_constructType} silently excluded from "Supported" (declared and behaviourally unchanged, just outside the v1 surface).@recordstays 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 thehow-to/source-row.adocrecipe, and stripped every now-danglingxrefand 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 danglingxrefsurvives.directives.graphqlsis untouched and no classify-time rejection was added. Recovery is anchor-free (git log --diff-filter=D+checkout <commit>^ -- <path>) and ticketed under R403 (@tableMethodrethink + recover), R404 (@sourceRowrecover), 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’sDirectiveDocCoverageTestdeclared-directive ↔ reference-page bijection (a hard build failure); fixed by76d5ac9, 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 generatedsupported-directives.adocfragment (declared − mentioned = withheld) so the test cannot drift from the report that ownsWITHHELD_FROM_V1. Coverage:DirectiveSupportReportTest(exclusion took effect: withheld trio + rejected pair absent from "Supported", rejected pair under "Removed / rejected",@recordretained) and the carved-outDirectiveDocCoverageTest; 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 fixture3586c06; pipeline tests6aa4bb3; compositejti_*re-authord08eacb; rejection tests521dd59; corpus + architecture-doc prosee163d59; In Review5451c0f; self-reviewbe47c31; schema-version rework3dc0a3a): First-class discriminated joined-table (class-table) inheritance, where each participant declares its own detail@tabledistinct from the discriminated base and its inherited (base-resident) fields carry@referenceback to the base. A newParticipantRef.JoinedTableBoundsealed variant carries the resolved child→parentJoinStep.FkJoin; residence is declared by the per-field@reference(base-resident →ColumnReferenceField, detail-resident → plainColumnField) and read off the field variant, never recomputed, so no residence-aware resolver was added toFieldBuilder.TypeBuilder.buildParticipantListdetects the detail-table participant, resolves the hop, skips the cross-table pass, and surfaces PK=FK / same-base / no-nameable-join violations asINVALID_SCHEMAdiagnostics with candidate-FK hints. The interface fetcher selectsFROM basewith a per-participant discriminator-gatedLEFT JOINto 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-columnparty/party_individual/party_companyfixture and the compositejti_*fixture re-authored to R389 (subsuming the R388 workaround). Coverage:@ExecutionTierallParties(routing + per-participant projection + NULL-through),allIndividuals(standalone), and the convertedallSubjectscomposite cases against real PostgreSQL;@PipelineTierJoinedTableInheritancePipelineTest(positive shape, mixed discriminator-only + joined participant, three rejection invariants); ajoined-table-interfaceR281 corpus example +code-generation-triggers.adocprose; no code-string assertions on generated method bodies. Independent-session In Review → Done review across two cycles: the first requested rework for a missingjooq.codegen.schema.versionbump (R389 changedinit.sqlbut left it at 2.2, so jOOQ skipped catalog regeneration on incremental-Plocal-dbbuilds → anUnclassifiedTypecascade across 11 tests); fixed by3dc0a3a(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 switche57bdbd; empty-body guardd6254dc): Newgraphitron-jakarta-restmodule, a reusable spec-conformant GraphQL-over-HTTP serving library over a Graphitron schema, ending the four-dialect drift between the reference app and thetilgangsstyring-style consumer copies (each had diverged on media types, status codes,/schema, and GraphiQL). The dependency-inversion seam is a consumer-implementedGraphitronApplicationSPI (schema(),newExecutionInput(), defaultengineBuilder(), defaultgraphiqlEnabled()) plus anAbstractGraphitronApplicationbase 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 (modernapplication/graphql-response+json: unparseable → 400, malformed/validation/coercion → 422, executed → 200; legacyapplication/jsonalways 200), GET mutation → 405,/graphql/schemaviaSchemaPrinter, and a CDN-based GraphiQL page; an application-scopedGraphqlEnginecaches the builtGraphQL. No custom JAX-RS providers: the resource reads the raw body and parses aGraphqlRequestrecord so it can shape parse errors as spec4xxand 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-providedstance. The module is the first hand-written runtime artifact consumers depend on, a third Java-version category (rewrite-design-principles.adocgrew the bullet): it compiles at<release>17</release>(the Java-17 floor consumers share), is publishable, and joins the deploy set.graphitron-sakila-examplerefactored onto it (itsGraphqlResource/GraphqlEnginecopies and self-hosted GraphiQL assets deleted, replaced by a one-classSakilaGraphitronApplicationadapter). Batching stays out of scope (the spec defines none). Coverage: an 11-case@ExecutionTierGraphQLOverHttpConformanceTestrun 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@DisplayNameplus a requirement → section → test pointer table; the library itself carries no@Testclasses 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-executionErrorType. Independent-session In Review → Done review; full reactor green undermvn -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 sandboxrewrite_testDB missing R389’sparty_*tables, resolved by a re-seed + clean rebuild, not a code defect in either item). -
R401 (Half A
8a08759+ dispatch fixf225eeb; Half Be6cbe70; In Reviewe288bf4): Bundle the tree-sitter runtime in the natives jar so the LSP has zero native system dependency.no.sikt:graphitron-tree-sitter-nativesnow shipslibtree-sitteralongside the grammar for all four supported platforms (eightlib/<os>-<arch>/entries), built in CI from the pinned upstreamv0.26.9source tag (POSIXmake; Windows MinGW-w64 with static-linked gcc runtime), gated on exported-symbol + transitive-dep allowlist assertions and apost-deploy-verifyload+parse on all four platforms with no systemlibtree-sitterpresent;0.26.9-1published to Central by a human-dispatched release.BundledLibraryLookupextracts both binaries and returnsgrammar.or(bundledRuntime)(system probe deleted);GraphqlLanguagecollapses 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, withDOCS_URLrepointed to the reference page. Thelsp-requirements, getting-started#native-runtime-dependency, and reference-index docs collapse to "nothing to install" (per-platform install matrix and NixOSshell.nixsnippet 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 nativespom.xmlheader comment and the UPSTREAM.md Windows build command, which still described the pre-bundle grammar-only module); fullgraphitron-lspsuite + reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. Supersedes the system-dependency model from R203. -
R269 (
077f8f3implementation; Spec → Readyfa6f38d, spec revisions81ba814, spec refresh1ac70fb): Null-guard the record-parent split-query accessor key extraction so a nullable to-one/to-many@tablerelation that resolves to no row renders null/[]instead of NPEing.GeneratorUtils.buildAccessorKeySingle(theONEarm, 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 returnednullon an otherwise-successful parent the emitted fetcher threwCannot invoke "...Record.into(...)" because "element" is nullon the success arm rather than resolving the field;buildAccessorKeyManyhad the analogous hazard one level out, the bare for-each over a never-populated to-many backing NPEing before any.into(...). TheONEarm now emitsif (element == null) return CompletableFuture.completedFuture(null);between the accessor read and the key build, mirroring the FK-sidebuildKeyExtractionWithNullCheckprecedent (a key that can’t match the terminal PK must not dispatch the loader; the to-one’s faithful "no row" rendering isnull, and the fetcher’sCompletableFuture<DataFetcherResult<Record>>return makescompletedFuture(null)assignable). TheMANYarm took design fork (a): hoist the accessor result to a typedIterable<Element>local and skip the for-loop when it is null sokeysstays empty and the existingloadManydispatch renders[](the extraction block does not own its return path; bothDataLoaderFetcherEmitterandMultiTablePolymorphicEmitterappend their own dispatch, so skipping the loop is the consumer-agnostic shape). The asymmetry is deliberate and documented in both helpers: theONEarm preserves null-vs-present (a to-one’s "no row" faithfully rendersnull) while theMANYarm 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/SourceKeychange; 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 sharednull key → completedFuture(null)seam acrossbuildKeyExtractionWithNullCheck/buildAccessorKeySingleis named as a drift-prone follow-up, left out of scope as a refactor with its own blast radius. Coverage: execution-tierGraphQLQueryTest.inventoryById_filmCardDataNullAccessor_rendersFilmNullWithoutNpe(aFilmCardDataaccessor returningnullfor evenfilm_id`s renders `filmnull with no error, oddfilm_id`s still resolve their `Filmrow through the same loader, the mixed-batch proof) andAccessorDerivedBatchKeyTest.accessorDerivedManyPayloads_nullToManyBacking_rendersEmptyListWithoutNpe(aCreateFilmsPayloadwith a nullfilms()backing renders[], the present sibling still resolves); theRecordTableField+AccessorCall+Cardinality.ONE/MANYclassifier shape the guards depend on is already pinned byGraphitronSchemaBuilderTest’s `AccessorDerivedSourceCasematrix; 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/__kdunders. Independent-session In Review → Done review; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(GraphQLQueryTest286,AccessorDerivedBatchKeyTest2, 0 failures). -
R262 (
c71885c, In Reviewc453aaf, Spec → Ready498ddf7, Backlog → Specd8ed14d): Reject@nodeIdon non-IDcoordinates and federation encoded@keyfields at validate time. The SDL directive permits@nodeIdonFIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITIONwith noIDrestriction, but every decode/encode arm is gated on"ID".equals(...), so a non-ID@nodeIdwas 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)inGraphitronSchemaBuilder(sibling torejectCaseInsensitiveTypeCollisions, same orchestration region) walksctx.schemaapplied directives across all threeon-locations, object/interface field definitions plus their arguments plus input-object fields, and registers anINVALID_SCHEMAValidationErrorfor any whoseGraphQLTypeUtil.unwrapAllis notID(findings F, G, and the outputFIELD_DEFINITIONencode mechanism that has noIDgate at all); it reads the raw schema rather than the registry precisely because a dropped@nodeIdleaves no trace on the classified field.EntityResolutionBuilderrejects a federation@keywhose referenced field resolves to aChildField.ColumnReferenceFieldcarryingCallSiteCompaction.NodeIdEncodeKeys(finding H, where the_entitiesDIRECT path would bind the encoded global id undecoded into the VALUES table) fatally via a newAltResult.Fatalarm carrying the typedRejection.invalidSchema, distinct from and not co-located with the existing non-fatal compound-idBuildWarning; the column lookup is refactored fromlookupColumntocolumnOf(field)so the caller can inspect the field instance before reducing toColumnRef. Decode-into-rep for encoded@keyvalues, the cast-axis defects inwire-coercion-cast-guard, and consolidating the replicated@nodeId-site predicate stay out of scope. Coverage: pipeline-tierRejectNonIdNodeIdPipelineTest(6 tests, asserting the typedValidationErrorcoordinate +RejectionKind+ message for the non-IDinput field, argument, and output field plus the federation encoded-@keysub-case, and the legitimateIDinput/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;RejectNonIdNodeIdPipelineTestgreen 6/6 in isolation under-Plocal-db. NB: the full reactor is currently red on threeJoinedTableInheritancePipelineTestfailures 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 (@erroron bare-entity query fields) and R273 (NodeId mismatch semantics). -
R378 (
3ca8428; Spec → Readye5dcd59, Backlog → Specf146e1e): Authored@nodeIdfilters now throw on a malformed or wrong-type id instead of silently dropping it to the unfiltered baseline (the reportedsoknadId: ["IKKE_EN_ID"]returns-the-whole-table bug). The four authored filter producers (argument-level same-table and FK-target inFieldBuilder, input-object-field same-table and FK-target inBuildContext) flip fromCallSiteExtraction.NodeIdDecodeKeys.SkipMismatchedElementtoThrowOnMismatch; the Relay heterogeneous-id-source pattern is given up deliberately per the user decision.CompositeDecodeHelperRegistry’s `Mode.THROWbody is enriched once (list and scalar arms) with a two-branch message computed fromNodeIdEncoder.peekTypeIdon 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 expectedtypeIdis threaded ontoHelperRef.Decodeas 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@errorlift, R397): a generated<outputPackage>.schema.GraphitronClientException(subclass ofgraphql.GraphqlErrorException, so natively aGraphQLError, channel-matchable, serialisable into theerrorsarray,serialVersionUIDemitted for clean-Xlint:serial) is the stable client-error marker the THROW arm raises; newErrorRouter.surfaceClientErrorOrRedactwalks the cause chain and surfaces aGraphitronClientException’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(renamednoChannelCatchArm),MultiTablePolymorphicEmitter, andChannelCatchArmEmitter’s empty arm. Deliberate boundaries held: the two `__NODE_*synthesis-shim arms stay onSkipMismatchedElement(R273/shim-retirement track), andLookupValuesJoinEmitter’s separate N×M decode-throw site keeps its plain `GraphqlErrorExceptionand 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-tierGraphQLQueryTest(malformed surfaces the real message naming the bad value +not a valid Film id; wrong-type surfacesdecodes to type "FilmActor"/expected a Film id; mixed surfaces; input-object-field filter surface exercises thesoknadIdshape; 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-tierNodeIdPipelineTest/IdReferenceShimClassificationTest(authored arms pinned toThrowOnMismatch, shim arms held atSkipMismatchedElement), unit-tierCompositeDecodeHelperRegistryTest+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 undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(GraphQLQueryTest283,:graphitron477 /:graphitron-sakila-exampletiers all 0 failures). Orthogonal to R375; predecessor of R397. -
R121, R296 (Backlog items, discarded as superseded by R398): the redundant-
@splitQuery-on-@recordLSP 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@splitQuerywarning 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 (
27325cd3eimplementation, fixture-comment rework7febb70b3; Spec → Ready05e652893, Backlog → Specbd0287f3e): 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 inTypeFetcherGenerator(the__discriminator__routing projection inbuildInterfaceFieldsList, the... IN (knownValues)restriction inbuildDiscriminatorFilter, and the cross-table LEFT JOIN ON-clause gate inbuildCrossTableJoinChain) to qualify viatableRef.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 withmissing FROM-clause entry(reported against10.0.0-RC21by the opptak consumer:@table(name: "INNBOKS_MELDING")overkommunikasjon.innboks_melding). All three sites now emit<tableLocal>.getQualifiedName().append(DSL.name(col))typedObject.class(so the.as/.in/.eqchains 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 theTypeResolveroff 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 inTypeFetcherGeneratorTestpin each site to the table-instance qualifier and forbid the directive-name string via a case-mismatchedINTERFACE_BASEfixture (the case/schema-mismatch dimension lives here); a non-default-schema execution guardMultiSchemaQueryTest.signalsRouteToDiscriminatedTypesUnderNamedSchemaover a newmultischema_a.signal@discriminateinterface (AlertSignalcarrying a cross-table@referencetowidget,NoticeSignal) covers the dimension R388 regressed on; the default-schema guardsPolymorphicProjectionQueryTestandGraphQLQueryTestallContent/allSubjectsconfirm 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@referenceFK-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 undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R386 (
c68b000implementation; Spec → Ready270b0c1, Backlog → Spec3c6430b, filed27d9c9e):catalog.searchMCP tool (R118 slice 10) ; the semantic counterpart to the structuredcatalog.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.CatalogDescriptorsis a pure, ONNX-free composer that turns each R362CatalogFacts.Tableinto one readable descriptor carrying both the raw SQL token (so BM25 matchesfilm_actorexactly) and its normalized words (a state-machinesplitWordssplitting 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-256corpusHashis length-prefixed over the exact descriptor strings handed toembedDocuments, so the hashed thing and the embedded thing cannot drift.CatalogSearchIndexowns the warm-managed, self-observing Lucene index, mirroring R374’sReverseEdgeIndex.Cache(noBuildArtifactscomponent, noWorkspacefield, noDevMojolistener): eachsearchreads the livecatalogFactsthrough 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 anAsyncWarmre-embed off the classpath-watcher thread, re-entering the existingWarmState.Warmingshape (no new "refreshing" state) so the priorReadyindex keeps serving while the new one builds. The index persists under${project.build.directory}/graphitron-mcp-rag/catalog/<corpusHash>/as a LuceneFSDirectory(survivesdevrestarts, dies onmvn clean), with an embedder-identity manifest (getClass().getName()+dimension()) written beside each index so loading a futuremultilingual-e5-smallindex 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 takesquery(required) +limit(default 10), returns{status, results:[{id, schema, name, comment?, score}]}by the same schema-qualified SQL idcatalog.describeaccepts (discovery hands off to description), and returns the sharedWarmState.degradationMessage+{status: warming|failed}while the index is notReady. Threaded throughGraphitronMcpServer’s widened five-arg constructor (RagConfig` as a growable record, back-compat overloads default it to a temp dir) andDevMojo.bindServer(supplying the build-dir cache root); the multilingual swap is flagged, not done. Coverage: unit-tierCatalogDescriptorsTest(split normalization cases, comment-present vs name-only degradation, raw+normalized tokens, length-prefixed hash stability/segmentation) andCatalogSearchIndexTestoverFakeEmbedder+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-warmFailedpropagation), MCP-handler-tierGraphitronMcpServerTest(catalog.searchadvertised intools/list; a ready-arm call returns rankedschema.tableids whose top feeds a follow-oncatalog.describe; the warming-arm call returnsstatus: warming; structured-content assertions only), and infrastructure-tier@Tag("slow")CatalogSearchOnnxTest(real bge ONNX embeds a Sakila-shapedCatalogFacts, assertspublic.address/public.paymentrank for natural-language queries ; the retrieval-quality + normalization payoff pin); no code-string assertions on generated bodies.getting-started.adocgains the agent-facing tool note (semantic search, the warming/refresh behaviour, the jOOQ-comment-capture lift). Builds on R372 (RAG foundation:Embedder+ the LuceneEmbeddingStore), R362 (CatalogFacts+ the schema-qualified ids), and R341/R361 (thegraphitron-mcpmodule + 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 undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R392 (
d832aaa, In Review flip2a808d4): Route the discriminated single-table interface (@table+@discriminate)TypeResolveroff 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 generatedTypeResolverread the discriminator with a barerecord.get(DSL.field(DSL.name(col))). When the interface also exposes the discriminator as a queryable field, the participant$fieldsprojects 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 logsAmbiguous match found, resolving to the first by luck. Fix mirrors the multi-table__typenameconvention: a sharedMultiTablePolymorphicEmitter.DISCRIMINATOR_COLUMN = "__discriminator__"constant (declared with its collision rationale, reaching generated code only as string literals ; a.as("__discriminator__")projection and arecord.get(DSL.name(...))read), projected under that alias inTypeFetcherGenerator.buildInterfaceFieldsListand read back inGraphitronSchemaClassGenerator, 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-tierGraphQLQueryTest.allSubjects_discriminatorFieldInsideFragment_routesViaSyntheticAlias(discriminator field selected inside the inline fragment; asserts routing per type plus theas "__discriminator__"projection viaSQL_LOG) and the schema-generator unit testbuild_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@tablejoined-table inheritance remains R389. Independent-session In Review → Done review; full reactor green under-Plocal-db. -
R383 (
6df5616implementation, In Review flip0770918; Spec → Ready0657369): Support nested-input@fieldfilters 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)withOccupantFilter { firstNames: [String!] @field(...) }) lowered to aNestedInputField(filter -> firstNames, leaf)call-site extraction thatFieldBuilder.firstUnsupportedFilterArgrejected 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.isBranchSafeExtractionbecomes a recursive switch where aNestedInputFieldis branch-safe exactly when itsleafis (Direct/EnumValueOf/ContextArgadmitted; aNodeIdDecodeKeysleaf stays rejected through the recursion, a developer@conditionstays rejected by theGeneratedConditionFilterguard), and the switch is exhaustive over the sealedCallSiteExtractionwith nodefault(the five non-branch-safe permits listed as explicitfalsearms) 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 asCallParam.emitsUncheckedCast()(single source of truth, Generation-thinking); both hosts (MultiTablePolymorphicEmitter’s `buildMainFetcher/buildRootConnectionFetcherand the single-tableQueryConditionsGenerator) fold over their call params and ask the model rather than each re-derivinglist() && 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>Conditionsmethod is byte-identical whether the value arrives top-level or Map-traversed. Coverage: pipeline-tierMultiTableFilterLoweringTest(nestedInputFieldFilter_lowersPerParticipantWithNestedExtractionasserts theNestedInputField(filter -> firstNames, Direct)call param per participant;nestedInputFieldCondition_rejectedStructuralNotDeferredkeeps a nested-input developer@conditiona structural author error) and execution-tierMultiTableFilterExecutionTestoverAddressOccupant = Customer | Staff(occupantsByFilter(filter: { firstNames: [...] })filters per branch and returns one matching row each, an empty filter narrows by nothing), with a new sakilaOccupantFilterinput +Query.occupantsByFilterfixture; no code-string assertions on generated bodies. The converted (JooqConvert/ ID-typed),@nodeId-decoded, and developer-@conditionkinds 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 undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R390 (
5485578, In Review flip92e7e16): Retain the connection carrier element subgraph in the rebuilt assembled schema. A@tabletype reachable only through a directive-driven@asConnectioncarrier was pruned bySchemaTransformeronceConnectionPromoter.rebuildAssembledForConnectionsretyped the bare-list carrier to name its synthesised Connection: the element type then hangs off the Connection’snodes/ Edge’snodeGraphQLTypeReference`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>Typeschema class never emitted, the consumer’s RC20cannot find symboljavac 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@asConnectionrewrite 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 bycarrierElementType, null-degrading when the parent/field/base type can’t be found) as aGraphQLSchema.additionalTypebefore 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-tierNestedConnectionElementRetentionPipelineTest(2: aStore --@asConnection--> Customer --@reference--> Paymentnested-reference chain retains both element types in the rebuilt assembled schema; a--@asConnection-->nested chain retypesCustomer.paymentstoCustomerPaymentsConnection!rather than leaving a bare list, with the synthesisedConnectionTypeclassified), structural assertions only, no code-string assertions on generated bodies. In Review → Done reviewed by a session distinct from the implementer; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R385 (
b0afcf8implementation,6cb322aREADME regen,d751110rework, In Review flipee71c81; Spec → Ready5cdcbe5, Spec2f014ce, filed27d9c9e):docs.searchMCP tool (R118 slice 9) ; build-time.adocchunking + 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.AdocChunkeris a pure(adoc, sourcePath) -> List<DocChunk>that walks raw.adocheading syntax (no AsciiDoctor render, so it stays off the docs module’s JRuby cost), splitting on section boundaries with an opt-in// rag:splitoverride, 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 toprocess-classesviaexec-maven-plugin, reading the public manual under a declared<docs.source.dir>) chunks + embeds viaBgeEmbedder.embedDocumentsand writes the bundle totarget/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 LuceneFSDirectory(unreadable from a jar, would force temp-dir extraction),DocsBundlepackages pre-embedded(id, embedText, payload, vector)tuples behind a(magic, version, dimension, count)header with explicit byte-length string prefixes (past thewriteUTF64 KB ceiling); the docs warm rebuilds an in-memoryLuceneEmbeddingStore.inMemory(dimension)by re-add()-ing the tuples, re-embedding nothing at runtime. The store-opaquepayloadis 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.DocsSearchToolembeds the query (embedder.embedQuery), runs theEmbeddingStoreseam, and returns ranked passages withheadingPath/sourcePath/anchor/text/score/ ahttps://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-Readyreturns the sharedWarmState.degradationMessageand no hits, leaving the dev loop structured-only. Wired throughGraphitronMcpServer’s new structured-only / injected-warm two-constructor seam and owned by `DevMojo. Rework (returned to Ready atdf7525a, re-landed atd751110): the first In Review pass failed the gate on a red build ;DevMojoTestpaid a realBgeEmbedderONNX 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 onDevMojo(mirroring the server’s injected-warm pattern;mojoFordefaults to structured-only null warms, the bind-failure test swaps in ONNX-free fakes) and anawaitAndCloseWarms()that joins each warm to its terminal state and closes the warmed docs store on the bind-failure unwind before rethrowing. Coverage: unit-tierAdocChunkerTest(5: nested heading paths,// rag:splitoverride, 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-onlyreadDimension), pipeline-tierDocsRagWarmPathTest(the productionloadDocsIndexloader end-to-end), MCP-handler-tierGraphitronMcpServerTest(warming/failed/ready/dimension-mismatch cases, structured-content assertions), andDevMojoTest(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")BgeEmbedderOnnxTestcovers the real load).getting-started.adocgains a paragraph. Builds on R372 (RAG foundation) and R341/R361 (thegraphitron-mcpmodule + live-Workspaceseam); sibling of R386 (catalog.search, slice 10). In Review → Done reviewed by a session distinct from both implementer sessions; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(DevMojoTest 6, GraphitronMcpServerTest 37, AdocChunkerTest 5, DocsBundleTest 3, DocsRagWarmPathTest 1; no SIGSEGV). -
R379 (
6f68611; Spec → Ready713d464, spec revise07c7dd3, Spec/filingdb865ef/1d8a78f): Validate that an@referencepath’s joins compile, moving twojavac-in-generated-code failure modes to build-time classification. Check 1 (terminal hop lands on the return table):InlineTableFieldEmitter.buildArmfeeds the terminal hop’s alias to a$fieldsoverload typed for the field return type’s@table, so a terminal hop landing elsewhere (theNusGrupperingFagfelt @table("NUSFAGFELT")reached via a path ending onNUSFAGGRUPPEreproduction, found in a downstream subgraph build) compiled to an incompatible-types error in a consumer’s build.BuildContext.parsePathnow computes a typedTerminalTargetVerdict(Match/Mismatch(fieldName, terminalTableName, returnTableName)/NotApplicable) over R232’s already-resolved terminalJoinStep.HasTargetTable.targetTable()and threads it ontoParsedPath, never re-deriving the hop kind from the directive element; theMismatchdiagnostic is formatted from the record’s fields so message and projection cannot drift. Check 2 (condition-method parameter tables): a two-argument condition method (aConditionJoinON clause or anFkJoin.whereFilter) emitted positionally asmethod(sourceAlias, targetAlias)that concretely types a jOOQTableparameter must match the alias the emitter passes it;validateConditionParamTables/checkConcreteParamTablecheck parameter 0 against the hop source and parameter 1 against the hop target at each condition resolution site, skipping the idiomatic wildcardTable<?>signature (unverifiable, fully accepted) and routing mismatches through the existingerrors→Rejection.AuthorError.Structuralchannel. Deviation (scope correction, agreed at review): the spec’s draft said to self-reject insideparsePath, butparsePathis 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,TableInterfaceTypeinFieldBuilder), 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-tierReferencePathTerminalTargetTest(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) andReferencePathConditionParamTest(6: both carriers and both parameter positions rejected, wildcard(Table<?>, Table<?>)and matching-concrete happy paths) over deliberately-mistypedTestConditionStubfixtures, 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 undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R363 (
2ede6e4implementation,73bc057self-review scope-tightening; Spec → Ready66cb717, spec revises6a3076d/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 bareUNION ALLand returned every row. The filter surface is per participant, not a single shared list: the same logical@fieldfilter resolves to a different table-specificWhereFilterper participant (e.g.FeideApplikasjonConditions.…vsMaskinportenApplikasjonConditions.…), so a new field-localmodel/ParticipantFiltersrecord pairs eachParticipantRef.TableBoundwith the filters lowered against its own table; the carrier is not a component on the type-scoped sharedParticipantRef, and the two fields stay offSqlGeneratingField(their return type isPolymorphicReturnType, a sibling of theTableBoundReturnTypethe capability’sreturnType()requires, soimplementswould not compile).FieldBuilderlowers once per table-bound participant viaresolveTableFieldComponentsagainst the participant table with a participant-named conditions class (participant.typeName(), notelementTypeName, so the per-participant methods do not collide), surfaces any participant’sRejected(absent or type-incompatible column) as the field’s rejection, and dedupes the@asConnectionsame-table advisory across the N participant calls.@condition(field- or argument-level) is rejected before the per-participant loop with a non-deferredRejection.structural(no danglingplanSlug), sinceresolveTableFieldComponentsitself lowers a@conditionbound 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.MultiTablePolymorphicEmitterthreads a typename-keyed filter map into both branch loops, ANDing each participant’s predicate into itsstage1_<Type>branchWHERE(buildStage1Blockcombines it with the existing parent-FK predicate;buildStage1ConnectionBlockgains a per-branchWHEREit never emitted before);TypeConditionsGeneratoris wired to the polymorphic fields'participantFilterssince they are notSqlGeneratingField. The self-review pass narrowed day-one extraction scope to the branch-safeDirect/EnumValueOf/ContextArgkinds:FieldBuilder.firstUnsupportedFilterArgstructurally rejectsJooqConvert(deprecated-for-removalDataType.converttrips the consumer-Werror),NodeIdDecodeKeys(needs theCompositeDecodeHelperRegistry), and nested-input / developer@conditionfilters (the classifier-guarantees-emitter-assumptions floor that letsbranchFilterWheredrop all registry/alias plumbing), with R383 filed for lifting the rest.orderByis split to R382 (__sort__doubles as the connection cursor seek key). Coverage: pipeline-tierMultiTableFilterLoweringTest(per-participant lowering for interface + union; absent-column rejection; ID-typedJooqConvertrejected structural; field- and arg-level@conditionrejected structural, not deferred) and execution-tierMultiTableFilterExecutionTest(AddressOccupant = Customer | Staff, list +@asConnectionforms filter per branch and return only matching rows, exercising bothbuildStage1BlockandbuildStage1ConnectionBlock); no code-string assertions on generated bodies. SharesMultiTablePolymorphicEmitterwith R365/R366/R367. In Review → Done reviewed by a session distinct from the implementer; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R374 (
5193fdc; Spec → Ready44b0146, Backlog → Specfbb6c1f, filedb106ccc): MCP cross-reference edges (R118 slice 7): the traversal layer over the frozen R362/R368 structured tools, all module-local tographitron-mcp. A newedgestool (D-A, a dedicated tool rather than aneighboursfield retrofitted onto every result, so the eight existing contracts stay frozen) takes exactly one node selector (field/type/table/column`+table` /method/class) plus adirection(out/in/both) and returns that node’s typed neighbours. D-D: a sealedNodeRefmodel (TypeNode/FieldNode/TableNode/ColumnNode/MethodNode/ClassNode) owns the whole stable-ID grammar and composes each wire string only at theMcpWireboundary (methodRef/ newcolumnId), reconciling the classifier’s bare table names throughCatalogFacts.resolve(the qualifiedschema.tableIDscatalog.describeaccepts) and its arity-free(class, name)method pairs through the external-reference scan (oneRESOLVESedge per overload). D-B: anEdgeKindlabel enum (BACKS/TARGETS/REFERENCES/RESOLVES/PARTICIPATES) with the varying endpoint shape pushed entirely intoNodeRef.target, so the enum carries no kind-dependent nullability (the sealed-over-enum tension resolved); theTARGETS/REFERENCESsplit falls out of the classifier’sjoinPathdistinction rather than being re-derived. The arm-to-kind mapping is an exhaustive no-defaultswitchover everyFieldClassification(28) andTypeClassification(23) permit, mirroringSchemaView; the cross-module drift guard: a new classifier permit fails the edge switch to compile. D-C: a lazy,(snapshot, catalogFacts)-reference-pair-memoisedReverseEdgeIndex(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 newBuildArtifacts/Workspacefield. Stage 3 (neighborhoodsubgraph tool) and indexing the forward-walkablePARTICIPATES(type → type) direction correctly deferred per R118 OQ6. Deviation from the spec’smcp/edges/sub-package: the edge model stays in the flatmcppackage to reuseMcpWire’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(forwardColumn/ColumnReference-with-joinPath / table-boundServiceBacked/@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-tierEdgeCoverageTestpartitioning 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-mcpmodule suite (52) and all 35 R374 tests green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(one full-reactor run flaked on the unrelated R372BgeEmbedderOnnxTeststrict-margin similarity assertion, which passes in isolation and on a focused module run; untouched by R374). -
R377 (
b44de9b; Spec → Ready2afdec9, Backlog → Spec212f21c, filed26e82a0):decode<typeId>mismatch when multiple@tabletypes share a table.BuildContext.resolveDecodeHelperForTableresolved throughfindGraphQLTypeForTable, an all-@tableindex 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 withNodeIdEncoderClassGenerator’s emitted `decode<TypeName>only when typeId equals the type name. A customized numeric@node(typeId:)over such a table (a@nodeplus a nesting-projection@table, e.g.UTDANNINGSMULIGHET) emitted adecode<typeId>call javac could not resolve, a latent error that surfaced only in the consumer’s compile, found portingutdanningsregisteretto Graphitron 10. Fix: rewrite the resolver to the@node-onlyNodeIndexby-table view (nodes.forTable), which is exactly the right domain (it sees only@nodetypes, not the projection types sharing the rows), with a three-way outcome: one node → itsdecodeMethod()(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 phantomdecode<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;findGraphQLTypeForTablestays for its one remaining caller (the id-reference synthesis shim). The casing divergence is closed structurally:NodeIndex.byTableis keyed on the lowercased@table(name:)echo at construction (TypeBuilder.buildClassificationIndices) andNodeIndex.forTablelowercases its lookup arg, so a consumer never re-establishes theTableRef.sameTablecontract. Coverage: three pipeline-tierNodeIdPipelineTestcases (decode-via-index-not-typeId assertingdecodeSharedNodenotdecode10154; multi-node rejection toUnclassifiedTypewith the "zero or multiple" message; orphan-input typeId fallback pinning the branch-1-drop decision) over a newnodeidfixture.shared_nodetable with a customized numeric typeId, plus agraphitron-sakila-examplecompilation backstop (FilmEndorsementNode @node(typeId: "920534")overfilm_endorsement, already backed by theFilmEndorsementprojection, decoded by a newendorsementsByNodeIdquery) 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 undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R375 (
8e0887acode,fe4303dplan-body test-name fixup; Spec → Ready0147443, Backlog → Spec4b443e9, filedb804f4c): Empty list passed to a fetch-path list-IN filter now narrows by nothing (DSL.noCondition()identity) instead of emittingIN (), which jOOQ renders as the constantfalseand 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@nodeIdfilter argument: the empty list AND-ed an unsatisfiable predicate into theWHEREand dropped every row. The rewrite has nohasIdsbranch (R50/e4b collapsed list@nodeIdfilters without@conditiononto a plain column-shapedBodyParam.In/RowIn), so the symptom was general to every list-IN filter, not@nodeId-specific. Fix: a literal empty guard on all fourIn/RowInarms ofTypeConditionsGenerator.buildConditionMethod(non-nullif (!arg.isEmpty()); nullable folds the emptiness into its existing!= nullcheck);Eq/RowEqunchanged (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 isnoCondition(the list-arity sibling of thenull/omitted case R230 already drops), while a lookup field’s input rows are the FROM-side of aVALUES…JOIN(LookupValuesJoinEmitter), where empty is an empty join domain and 0 rows is the only coherent answer;TypeConditionsGeneratoralready excludesLookupFieldupstream (line 63). The guard is emitted as a literal, not lifted into a sealedEmptyBehaviormodel 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 theWHERE 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 becauseIN () = falsecoincidentally zeroed the query; both inverted to the unfiltered baseline. The all-malformed case (SkipMismatchedElementdrops every id → emptyList<Integer>, indistinguishable from a literal[]at the condition method) follows the samenoConditionrule 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-tierTypeConditionsGeneratorTest(!ids.isEmpty()assertions on theInandRowInarms + newinFilter_nonNullList_emitsEmptyGuardWithoutNullCheckpinning the non-null arm the nodeId helpers don’t reach), execution-tierGraphQLQueryTest(invertedfilms_filteredBySameTableNodeId_emptyListReturnsUnfilteredBaseline, new connection regressionfilmsConnectionByOptionalIds_idsEmptyList_paginatesFullTableAndCountsAllasserting nodes +totalCountboth unfiltered, invertedfilmsByNodeIdArg_{emptyList,allMalformedIds}_returnsUnfilteredBaseline, and the lookup-divergence comment oninlineLookupTableField_emptyInput_returnsEmpty). In Review → Done reviewed by a session distinct from the implementer; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(GraphQLQueryTest 275, full suite 461 + 65, 0 failures). -
R376 (
64c6f23implementation,35a2e7estale-javadoc cleanup; Spec → Readyf3f2a26, spec refinementse60d49b52fc227, filed21c4e62): 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/@tableMethodfield (and its root query/mutation forms) previously had no field-name jump at all: the cursor had to be parked on the directive’smethod:/className:argument beforeDefinitionswould navigate. The bound class+method were already resolved on the snapshot’sFieldClassification; goto and hover simply never consulted it for the name trigger. Routing: a newDeclTarget.methodBackedTargetarm consultsbuilt.fieldClassification(parentType, member)before theTypeBackingShapedispatch inofField, 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.SourceMethodwidened 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 aDSLContext, which would have made goto jump while hover returned empty, violatingoverlayIsPresentExactlyWhenGotoJumps).SourceWalker.Indexgains a never-droppedmethodsByNameview andresolveMethod: 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.Ambiguousis retired: the directive-argDefinitions.methodTargetpath 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 usesdefault -> emptyover the 32-variantFieldClassificationrather than an exhaustive switch (a future method-backed variant silently won’t extend, scoped out by the spec forbidding aFieldClassificationmodel 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, soresolve()takes the first catalog candidate’s arity and the name floor still guarantees a jump. Spansgraphitron-lspplus the smallSourceWalker.Indexaddition ingraphitron. Coverage:DeclarationDefinitionsTest(the four named variants jump end-to-end; an arity-distinguishablegreet()vsgreet(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-aritySourceMethodparity case). In Review → Done reviewed by a session distinct from the implementer; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(graphitron-lsp suite + 461 + 65, 0 failures). -
R372 (
c2acec1; Spec → Ready5dadff3, Backlog → Specc154ce2, filed47e4e24): MCP RAG foundation (R118 slice 8): the semantic-layer infrastructure thedocs.search(slice 9) andcatalog.search(slice 10) tools sit on, all module-local undergraphitron-mcp/.../rag/, registering no agent-facing tool and leavingGraphitronMcpServeruntouched. Three seams plus one lifecycle. D1: a graphitron-ownedEmbedderseam (embedQuery/embedDocuments/dimension) that owns the bge query/document asymmetry ;BgeEmbedderprepends the bge instruction prefix on the query path only ; and whoseQuery/Embeddingrecords 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-onlybge-small-en-v1.5-q(384-dim) for V0. D2: anEmbeddingStoreseam withLuceneEmbeddingStore(BM25 + KNN in one index, fused by reciprocal-rank fusion) as the sole shipping backend, an in-RAMByteBuffersDirectoryinstance of the same class as the seam’s test fake, and the dimension invariant checked once ataddagainst the embedder’sdimension(). D3: a generic sealedWarmState<T>overWarming/Ready/Failedwith a handle-agnostic degradation-message helper (exhaustive switch, nodefault, rejectsReady), and theAsyncWarm<T>background-daemon harness whoseawait()returns the terminal value (neverWarming) so a dependent build-warm maps an upstreamFailedinto its ownFailedrather 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 ongraphitron-mcpalone, never the plugin’s compile surface; surefire on this module gains--enable-native-access=ALL-UNNAMEDwith noexcludedGroups, so CI’s defaultmvn verify -Plocal-dbruns 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,WarmStatetransitions across both type parameters + await propagation / cross-warm failure; infrastructure-tierBgeEmbedderOnnxTest(@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 (
ea47993implementation; Spec → Ready173a7d0, Backlog → Spec304c39f, filedd7c8d15): MCPcatalog.tables/catalog.describeover a build-timeCatalogFactsprojection (R118 slice 2, on the R361 seam). Resolves R361 D1 to build-time enrichment (option A, not a retained loader): a new frozenCatalogFactsrecord (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 inCatalogBuilder.buildCatalogFacts(JooqCatalog)while the codegen loader is open, carried as a thirdBuildArtifactscomponent besideCompletionDataand the snapshot, and swapped onto avolatile Workspace.catalogFactsfield insetBuildOutput. The load-bearing invariant (noTable<?>/ForeignKey<?,?>/Field/Classretained) is what lets the projection outlive the per-passwithCodegenScopeloader close; new resolved-immutableJooqCatalogaccessors (allTableEntries,candidateKeys(Table<?>),columnFactsOf,indexFactsOf,foreignKeyFactsOf) reduce every live handle toStringat the parse boundary.GraphitronMcpServerregisters the two tools mirroring the R361statusToolshape:catalog.tables(schema + SQL-name-substring filters, opaque base64-offset cursor paging withnextCursor) andcatalog.describe(resolved / ambiguous / not-found arms over a parallelCatalogFacts.TableResolution).sqlTypemaps to the jOOQDataType.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_binunique 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 mappedstructuredContentfor list/filter/page and describe resolved/ambiguous/not-found. In Review → Done reviewed by a session distinct from the implementer;mvn install -Plocal-dbgreen (CatalogFactsTest 9, GraphitronMcpServerTest 11, full suite 460 + 65). -
R365 (
3366bbefloor,976e0a3classifier+emitter,0e82f71execution fixture; In Review → Ready reworke7477ac, rework fixb28fe15, mutation fixture + drop-contractfd1f59e; Backlog → Specb6a6f93, Spec → Ready0426d42): Restore the graphitron 9.3 ability to return a polymorphic entity from a root@servicefield (route (a): the service hands back a PK-populated jOOQTableRecordper 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 `PolymorphicReturnTypearm now resolves to a newResolved.Polymorphicsuccess (the all-@errorerrors-channel lift still takes precedence) instead of the old "not yet supported" reject; newQueryServicePolymorphicField/MutationServicePolymorphicFieldleaves carry the participant set and service method, andMultiTablePolymorphicEmitter.emitServiceMethodsreuses 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@servicereturning a distinct-table multitable interface. Three guards keep that floor honest at the one sharedvalidateMultiTableParticipantssite and at classify: same-table participants in a plain multitable interface/union are anAUTHOR_ERROR(record-class dispatch cannot tell shared-recordClassparticipants apart, with or without@discriminator; model as a single-table discriminatedTableInterfaceTypeor split) ; this also guards R363’s query path; a@servicereturning a union is permanently unsupported (AUTHOR_ERROR, union polymorphism is a generated-query-path capability the service path never grew); a@servicereturning a single-table discriminated interface (TableInterfaceType) is deferred-rejected (the table-bound service path emits no per-row discriminator dispatch). Child@servicepolymorphic returns stay deferred (root only). The In Review pass requested rework on a silent-misdispatch hole (same-table participants carrying@discriminatorpassed the original no-discriminator-only floor and reached a dead-arminstanceofchain 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@ProjectionForprojection test, unit-tier floor pins inQueryInterfaceFieldValidationTest/QueryUnionFieldValidationTest(both same-table subsets →AUTHOR_ERROR), builder negatives (serviceReturningUnion_rejectedAsUnsupported,serviceReturningTableInterface_deferred), and execution-tierServicePolymorphicReturnExecutionTest(query single + list and a@servicemutation 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 inbuildServiceMainFetcher. SharesMultiTablePolymorphicEmitterwith 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) andgraphitron-sakila-example(460 tests) green under-Plocal-db;graphitron-lsp/graphitron-mcpnot 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 → Specb6a6f93, Spec → Ready88e1659, In Review → Ready reworkcb2b4b9): Single-cardinality polymorphic child on a record-backed (Pojo / JavaRecord) parent, closing the capability gap the generator deferred atFieldBuilder’s `!fieldIsListarm (whoseRejection.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.buildScalarPerParentFetchergains a record-parent arm: it now takesparentSourceKeyand, for aReader.AccessorCallparent, bindsparentRecordto the accessor’s returned hubTableRecord(((Backing) env.getSource()).<accessor>()) instead of casting the source to a jOOQRecord(which wouldClassCastExceptionon a Pojo source); a null hub yields a null payload, and the table-backed arm keeps the(Record) env.getSource()cast.FieldBuilder.resolvePolymorphicRecordParentdrops the deferral so both cardinalities route throughderivePolymorphicHubSource, removing the only reference to the dangling slug. Scope is top-level backing classes; the nested-backing-classOuter$Nestednon-compiling-cast hazard (shared with the list arm viaClassName.bestGuessover a binary name) is filed as R370. Coverage: the two pipeline-tier deferral assertions inRecordParentMultiTablePolymorphicPipelineTestflip to assert successfulAccessorKeyedSingleclassification (interface + union), and execution-tierAddressOccupantCarrierSingleCardinalityTestdrives a Pojo carrier holding anAddressRecordhub throughQuery.addressOccupantCarrier, pinningfirstOccupantto the firstCustomer|Staffby 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 queriedaddressOccupantCarrier(addressId: 4)butinit.sqlseeded 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 nestedAddressOccupantCarrierService.Carrier. Sibling of R366 (list cardinality); enables R365 shape (b); sharesMultiTablePolymorphicEmitterwith 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 → Spec87544d0, Spec → Readybf002a5, Ready → In Progressacef1bc):graphitron:devwalks generated-sources of scanned reactor modules so goto-definition / hover reaches jOOQ tables that live in a separate module from the schema module runningdev. Root cause was a lifecycle asymmetry between the two root setsAbstractRewriteMojofeeds the LSP:resolveClasspathRoots()readstarget/classes(on disk from any prior build, so every reactor sibling is scanned for completion), butresolveCompileSourceRoots()readproject.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/@fieldjumps landed onDefinitionTarget.SourceAbsent(silent no-jump), breaking the R351 scan/walk parity invariant. Fix is in the shared resolver, not a dev-only branch (D2): newgeneratedSourceRoots(MavenProject)does a lifecycle-independent disk scan of the existing immediate subdirs oftarget/generated-sources/(thegenerated-sources/<tool>convention every generator follows, not POM-config parsing ; D1), andcompileSourceRootsOf(MavenProject)is the single per-module "what is walked" definition (getCompileSourceRoots()∪generatedSourceRoots) that bothresolveCompileSourceRoots()and the newunwalkedScannedModules(Iterable<MavenProject>)route through, so resolver and diagnostic cannot drift;collectExistingDirsdedups by normalised absolute path so a root the plugin already registered (full-lifecycle goals) collapses, making the widening a no-op there.DevMojorenders a startupWARNnaming any residual scanned-but-unwalked module (the dependency-JAR case with no.javato walk, explicitly out of scope for the auto-include). The staleresolveCompileSourceRoots()javadoc was rewritten to match. Coverage: unit-tierAbstractRewriteMojoTest(5: disk discovery incl. stray-file exclusion, empty cases, the core widening regression, plugin-registered dedup, `+unwalkedScannedModules` reporting only the genuinely-unwalked module); unit-tierSourceWalkerTest.disjointGeneratorPackagesKeepTableJumpLocatedNotAmbiguouspins the D1 output-package-disjointness argument (a graphitron output root walked alongside a jOOQ root leaves the table-class jumpLocated, notAmbiguous) rather than leaving it as unpinned prose; no generated-body string assertions. Builds on R351 (parity invariant +collectExistingDirs) and R352 / R90 (SourceWalker.Index,DefinitionTargetempty-resolution contract). In Review → Done reviewed by a session distinct from the implementer.graphitronandgraphitron-maven-plugintest tiers green under-Plocal-db(AbstractRewriteMojoTest9/9,DevMojoTest6/6,SourceWalkerTest9/9); thegraphitron-lsp/DevServerTestcompletion-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 → Spec8d0f8e0, Spec → Ready60e0667): MCP shared-model seam, slice 1 of the R118 MCP programme. The R341 skeleton served static content only (promptscapability alone, an argument-lessaboutprompt) and held no reference to the live generator model. R361 widensGraphitronMcpServer’s constructor to `(InetSocketAddress, Workspace)and holds the live handle;DevMojo.bindServerpasses the sameWorkspaceinstance it hands the LSPDevServer, so the existing schema / classpath / source watchers refresh it in place with no new trigger, listener, or refresh path (thevolatilefields give per-field visibility on the next read). Thetoolscapability is declared (.tools(false), thelistChangedboolean) with one livenessstatustool that readsWorkspace.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 exhaustiveswitchover theLspSchemaSnapshotsealed permits with nodefaultso 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 seamWorkspace-only: the rawJooqCatalogis not threaded (it reflects lazily against thecodegenLoaderURLClassLoaderthatwithCodegenScopecloses each pass), deferred to slice 2. Adds the acyclicgraphitron-mcp→graphitron-lspcompile edge (plugin → {graphitron, lsp, mcp};mcp → lsp → graphitron), orthogonal to the module’s native-RAG dependency quarantine. Coverage: infrastructure-tierGraphitronMcpServerTestboots a real server and drives it with the real MCP client, assertingtools/listadvertises thestatustool andtools/callreturns the two-axis snapshot on both the defaultUnavailablearm and asetBuildOutput-drivenBuilt.Currentarm;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 lifecycle115d174,56e8fb6,b271054,07755b5, Spec → Ready83f0542): MCP server skeleton embedded ingraphitron: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. Newgraphitron-mcpmodule (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 on127.0.0.1:8488(the LSP’s is8487) at the/mcpendpoint, serving the handshakeinstructionsstring plus a single argument-lessaboutprompt, both read once at startup from bundled jar resources (mcp/instructions.txt,mcp/about.md) mirroringLspVocabulary’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-lspsplit also serves, not transport symmetry); it is published likegraphitron-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 nomaven.deploy.skip. Lifecycle wiring:DevMojo.bindServerconstructs the server as a sibling of the LSPDevServerandDevMojo.cleanupcloses it;mcpPortdefaults toDEFAULT_MCP_PORT = 8488but is deliberately not a@Parameter(a configurable port stays deferred). A taken MCP port fails fast with aMojoExecutionExceptionnaming 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-pasteableclaude mcp addline;graphitron-sakila-exampleships a committed.mcp.json. Coverage (transport-glue layer, outside the four-tier classifier/emitter enforcement by design): infrastructure-tierGraphitronMcpServerTest(3, boots a real server on an ephemeral port and drives it with the SDK’s own client:initializecarries the bundled instructions;aboutis advertised argument-less and returns the explainer; a taken port throwsIOException);DevMojoTestgains the Mojo-message + no-LSP-leak case and aDEFAULT_MCP_PORT == 8488pin ondefaultsMatchPlanContract. Docs:getting-started.adocgains 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.adocmodule count 9 → 10 andgraphitron-mcpadded to the publishable surface. Out of scope and staying in R118: livecatalog/schematools over the warmWorkspace, docs RAG, any vector store / embeddings / ONNX, the stdio-to-HTTP proxy, and the decision on keeping R118’s heavy deps off the non-devplugin 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; spec61d5a9c, Spec → Ready2ac746d, revise Ready → Spec2ad3d45, Spec → Readyd75c838, Ready → In Progresse2e9612, In Progress → In Reviewe23b203): 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@tablecasing vs the lowercase jOOQTable.getName()the record-class resolution path feeds in), and a case-sensitive.equalssilently mis-decides under an Oracle-style UPPERCASE@tableover a lowercase jOOQ catalog: the R357 bug, whichFieldBuilder.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 addTableNameComparisonCaseGuardTest(@UnitTier), a recursive source scan oversrc/main/java/no/sikt/graphitron/rewriteforbidding the.tableName().equals(spelling with a nonzero-scanned-file tripwire against a vacuous pass. Phase 2 (7202e20): move the comparison onto the type asTableRef.sameTable(String)/denotesSameTableAs(TableRef)(case-insensitive, null-safe canonical identity;tableName()stays the verbatim diagnostic echo), migrate all ~10 comparison sites acrossFieldBuilder/TypeBuilder/GraphitronSchemaValidator/BuildContext/NodeIdLeafResolver(both operand orientations, behaviour-preserving), and strengthen the guard to forbid every rawtableName()comparison (both.equals/.equalsIgnoreCase, both orientations) excluding the predicate’s homemodel/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-tierMutationDmlNodeIdClassificationTest#bulkDeleteIdCarrier_explicitNodeId_caseMismatchedTable_admits(explicit@nodeId(typeName: "Bar")with UPPERCASE@table(name: "BAR")NodeType over lowercase@table(name: "bar")carrier; the spec’s:3105reachability obligation, option (b)), verified rejecting pre-fix (the carrier drops to a non-SingleRecordIdFieldFromReturningclassification) and admitting post-fix (encodeBarwired, empty diagnostics); unit-tierTableRefSameTablePredicateTest(matching/mismatched casing both directions, null arg); the strengthenedTableNameComparisonCaseGuardTestas 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 identicalColumnRef.sqlName()sibling (one live.sqlName().equals(atGraphitronSchemaValidator.java:883alongside sixequalsIgnoreCasesites). CanonicalizingtableName()at construction rejected because it would change author-facing diagnostic casing (a documented invariant). Depends-on R357 (which convertsFieldBuilder.java:5114; R358 owns:3105and 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 add3b7d54a, Backlog → Spec8d8cb45, Spec → Readyfdad51d): Case-insensitive@table(name:)match in record-composite carrier accessor resolution. A@servicerecord-composite carrier (R329’s two-levelPayload { results: [Result], errors }/Result { @table children }shape) whose result-type@tablechildren declare@table(name:)in a case differing from the lowercase jOOQ catalog name misclassified every such child asUnclassifiedFieldwith the three-optionresolveRecordParentSourceauthor 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:expectedSqlNamecarries the verbatim@table(name:)casing (viaresolveTable) while the accessor’s elementTableRefcarries the jOOQTable.getName()casing (viaresolveTableByRecordClass), 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 toequalsIgnoreCase, the table-name idiom already used at eight other sites. Surfaced by the utdanningsregisteret Graphitron 10 migration. Coverage: pipeline-tierServiceRecordCompositeCarrierPipelineTest#caseMismatchedTableName_classifiesCompositeChildrenAsRecordTableField(the R329 FilmWithActors carrier with@table(name: "FILM")/@table(name: "ACTOR")against lowercasefilm/actor, asserting both children classify asRecordTableFieldONE/MANY with empty diagnostics, the verdict not the case-insensitivity mechanism), verified failing pre-fix (both fall toUnclassifiedField) 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(atFieldBuilder.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; canonicalizingTableRef.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 Review59e1cc3; 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 `collectExistingDirstraversal over the same reactor project set, so a class scanned for completion provably has its source root walked for goto-definition;DevMojologs 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/ReferencedropSourceLocationand instead carry the generated table-class FQN (and theKeys-class FQN for references);Definitionsjoins those FQNs against the LSP-ownedSourceWalker.Indexat request time and routes the@table/@field/@referencearms through the same exhaustiveDefinitionTargetswitch the service half uses, retiring the file-head0:0synthesis (a known table whose source is not on a walked root lands onSourceAbsent, a clean non-jump). Hover /descriptiononto the source cadence:CatalogBuilderno longer walks.javaat all (descriptions are the build-derivable fallback only: the jOOQ table’s SQL comment, empty for columns and services); a newDescriptionsoverlay 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), andHoversplus theFieldCompletions/TableCompletionsdetail 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 `Workspacealongside thevolatile sourceIndex;Workspace.refreshSourceIndexis the single walk entry point, called by the dev goal’s source-root watcher. Coverage favours real end-to-end over mocks:SourceCadenceHoverAndDefinitionTestwalks real.javathrough the realWorkspace/SourceWalker/Hovers/Definitions(asserting hover and goto move together across a source edit with the catalog the same instance, no rebuild);CatalogBuilderSourceTestinverts to pin the build-boundary decoupling (documented sources on the build are not lifted);SourceWalkerTestpins per-instance cache isolation (a same-mtime content change a static path+mtime cache would mis-serve);AbstractRewriteMojoTestpins thecollectExistingDirsclasspath/source-root parity;DefinitionsTestreaches every jOOQ arm and everyDefinitionTargetoutcome; 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 theDefinitionTargettyping); absorbed R352 (the jOOQ-half / hover-cadence / static-cache follow-up R349 deferred). -
R342 (
fa2743f; spece736a03, Spec → Readyde7490b): 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 bulkUPDATE 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’sv(…)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@fieldplus a@nodeIdFK reference whose lifted child column coincides), and a self-FK@nodeIdon a list-input UPDATE (rejected at validate time byUpdateRowsWalkerStage 2b). Plan:TypeFetcherGenerator.setColumnPlan(List<SetGroup>), the SET analogue ofinsertColumnPlan, groups the set groups' columns by backing-columnsqlNameinto an ordered writer list (each carrying its sourceSetGroupindex, slot, andColumnRef),shared()when ≥2 writers; the three bulk emitters (emitSetVColNameAdds,emitSetBulkCellAdds,emitSetVFieldPuts) all walk the one deterministic plan, so thev(…)column-name list, the per-row cells, and thesets.putentries 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 intoemitBulkSetDecodeLocals(INSERT-styleinstanceof Stringguard + 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-guardedappendAgreementValue), pairwise-checks them throughNodeIdEncoder.requireColumnAgreement, and adds the single coalescedDSL.val(firstPresent, col.getDataType())cell,emitInsertAgreementPrep’s coalesced-cell shape transplanted into the row loop (no `DSL.defaultValuebranch, since the conditional gate guarantees a present writer, and notemitSetAgreementPreamble’s check-then-let-the-puts-run shape, which the bulk derived table’s lack of a last-write-wins `Map.putaffordance forbids). Cross-partition (WHERE∩SET self-FK) fork (resolved in In Progress withprinciples-architect):setColumnPlansees 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,emitSetVFieldPutskeeps the no-opsets.put(keepssetsnon-empty so the empty-SET runtime guard does not fire on a minimal self-FK input), and a newemitBulkKeySetAgreementemits 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-FKUnsupportedInputFieldShapereject is deleted and the now-deadlistparameter dropped fromwalk(bothFieldBuildercall 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.UnsupportedInputFieldShaperetains its other producers. Coverage:UpdateRowsWalkerTest(15 pass: the bulk self-FK reject test inverted to admit-and-route-all-SET with the sharedmailbox_idin both partitions, plus a decode-involving-overlap-admits-without-PlainColumnCollisiontest); execution-tierBulkUpdateSetAgreementExecutionTest(7 tests: within-SET agree / disagree-rolls-back / asymmetric-present ×2 on thefilm_endorsementupdateEndorsementsOverlapfixture, self-FK agree-repoints-with-mailbox_id-no-op / disagree-rolls-back / omitted-nullable on theemailupdateEmailRepliesfixture), the list-input siblings ofNodeIdValueAgreementExecutionTest/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 /@servicepaths (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 → Spec8133ecf, Spec → Ready42efe84): Infer depth-1 nested@conditionarg bindings by name withoutargMapping. A@conditionwhose slot is an input object with scalar fields previously required an explicitargMappingto bind the condition method’s parameters to the nested fields, even when the parameter names already matched the nested field names exactly (the motivatingsearchVektingstallRange(table, fra, til)againstSokVerdiRange { 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 ANDmapToJavaTypeName(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 theunambiguousReachablePathsuggestion uses), it binds the parameter one level in viainferNestedFieldByName→PathExpr.step(head(slot), field, liftsList). The synthesisedPathExpris byte-identical to the one a hand-writtenargMapping: "p: slot.field"produces;liftsListis computed viaArgBindingMap.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 /argMappingsuggestion 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 explicitargMappingby 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 (thePathExprequals the explicit-argMappingone), but the execution test surfaced a latent gap shared by both forms: the input-field-@conditionrewrap 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 wrapperMapto the leaf type (a defensive-cast-that-throws, against "classifier guarantees shape emitter assumptions"). A newnestedPathhelper now appends the per-parameter path tail (segments after the head, the head naming the input field already atleafPath’s tail) to the `NestedInputFieldpath; a bare-head (single-segment) binding returnsleafPathunchanged, byte-identical for every pre-R355 binding, and the fix also completes the explicit-argMappingform for the same shape. The R214ServiceCatalogTestcase 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: unitServiceCatalogTest#inferNestedFieldByName_*(single scalar match, listliftsList=true, name-with-wrong-type → null, two-slot ambiguity → null); pipelineGraphitronSchemaBuilderTest(INFER_NESTED_CONDITION_ARG_BY_NAMEasserting the inferredPathExprchain, its*_EXPLICIT_ARGMAPPING_EQUIVALENTsibling asserting the identical values to pin equality,*_LIST_LIFTS_LISTpinning the computedliftsList=true,*_AMBIGUOUS_FALLS_THROUGHpinning the rejection, all on classifier-outputPathExprvalues not generated-body strings); executionGraphQLQueryTest#inputFieldCondition_nestedArgInferredByName_filtersSameAsExplicitArgMapping(arental_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-argMappingsibling). 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 (nestedargMappingsyntax, the explicit axis), R219 (unify arity-/type-unique under oneJavaTypeKeycount rule, the type-based inference this sits beside), R214 (the layered type-based inference this follows). -
R329 (
5b59e4a; spec268e404, revised21a0d36, Spec → Ready8c414b6): Re-admit@servicecarrier payloads with a record-composite data field, landing R75 Phase 3 on the post-R276 reflection-driven binding model. An@servicemutation whose method returns a list (or single) of a consumer-authored composite (a POJO bundling several jOOQ records, e.g. oneFilmRecordplus aList<ActorRecord>) is now expressible as a two-level carrier: a payload whose non-@tableobject data field is a list of an intermediate result type whose@field-mapped@tablechildren 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 sealedProducerBindLevel { BindsWrapper, BindsDataFieldElement, NoBind }read by both the single-level and two-level paths (theArgumentRef-style classify-once/project-into-each-consumer pattern), replacing the oldsdlIsList != reflectedIsMultireject + carrier-admit pair that would desync into a dangle or double-bind;BindsDataFieldElementgrounds the data field’s element type to the producer’s reflected return-element on the existing result axis, so no newProducerBindingarm and no nullable-TableRefgeneralization ofServiceEmitted. Carrier recognition:carrierTableBindingbecomes a sealedCarrierBinding { TableBacked, ClassBacked, NotACarrier }with onecarrierVerdictprojection shared bylookAheadVerdict, the producing-edge registration, and the nesting/orphan guards, so the verdict cannot drift; the composite carrier classifies as a class-backedResultTypenaming the per-element composite, gated on the payload being@service-produced (BuildContext.isServiceProducedPayload, the single producer of that fact, shared with the errorsWrapperArmselector) and not itself result-axis-bound. Emit side: a newChildField.RecordCompositeFieldleaf, justified by a dimensional row (Record/bareFetch/listOrSingle(Record)/Plain(composite)) distinct fromRecordField(Field target),RecordTableField(Table target), andServiceRecordField(ServiceCall operation); a source-passthrough projection carrying its ownSourceEnvelope(DIRECT/OUTCOME_SUCCESS) rather than recomputing it at emit, reusing the existingOutcomeWrapperArmfor errors withcomputeMutationServiceRecordReturnTypere-levelled to the reflected method return. Validator mirror: the three near-misses surface through existing recognizers (mismatched producer →RecordBindingMultiProducer; a@fieldchild neither@table-backed nor a resolvable composite accessor → accessor-mismatch; the re-levelled cardinality mismatch →checkServiceReturnMatchesPayload), not new predicates kept complementary by hope. Coverage: aClassifiedCorpusentry pinning the payload classification + data-field verdict, a@ProjectionFor(RecordCompositeField)LSP-projection assertion,RecordCompositeFieldinIMPLEMENTED_LEAVES(GeneratorCoverageTeststays exhaustive), pipeline-tier positive coverage (ServiceRecordCompositeCarrierPipelineTest: list + single arrival,DIRECT/OUTCOME_SUCCESSenvelope, errorsWrapperArm,@tablechildren asRecordTableField), validator-tier coverage of the three near-misses (ServiceRecordCompositeCarrierValidationTest), and an execution-tier round-trip ingraphitron-sakila-example(GraphQLQueryTest:List<composite>projection + the error arm renderingdata: 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-keyedMap<Key, List<composite>>shape stays out of scope for root carriers: a root@servicereturns the composite list directly, whilepeelReturnElement/isMultiCardinalityReturnalready peelMapfor the orthogonal child-batching path and aMapreturn at a root carrier rejects loudly viacheckServiceReturnMatchesPayload(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; spec64913b8, Spec → Ready8e46e7f): Self-FK@nodeIdon 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 @referenceon the INSERT/read sides and theemail/mailboxfixture). 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), wheremailbox_idis a PK member) previously trippedUpdateRowsError.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: aselfReferenceboolean onInputField.ColumnReferenceField/CompositeColumnReferenceField, set once at theNodeIdLeafResolverdiscrimination site (whereT.table()equals the containing table) and threaded throughBuildContext; every other construction site passesfalse(the fact-lives-in-the-model lift, the walker readscarrier.selfReference()rather than re-deriving self-ness). ThreeUpdateRowsWalkersites 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 failsNoUniqueKeyCoverage, 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 clearUnsupportedInputFieldShapereject rather than a silently-wrongFROM (VALUES …)derived table. The narrowedMixedCarrierKeyMembershipstill 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_admittedpins that the all-SET rule did not leak). Emit:TypeFetcherGenerator.emitKeySetAgreementPreambleadds 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’srequireColumnAgreement(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 (theemitAgreementDecodeLocal/appendAgreementValuehelpers 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 stillMixedCarrierKeyMembership),MutationDmlNodeIdClassificationTest(updateEmailReplyclassification + the marker on both carriers),graphitron-sakila-examplecompilation ofUpdateEmailReplyInputagainst real jOOQ at Java 17, and execution-tierSelfFkNodeIdUpdateExecutionTest(agree repointsin_reply_to_nowith themailbox_idSET a no-op / disagree throws and rolls back, no silent row-move / omitted nullable updatessubjectonly, no agreement check); no generated-body string assertions.docs/typed-rejection.adocnarrowed andRejectionSeverityCoverageTestannotated 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/@externalFieldclass or method reference silently no-opped in a livegraphitron:devsession when the reference’s source root was scanned for bytecode (so completion worked) but not walked for positions:CatalogBuilder.enrichExternalReferencesleft theCompletionData.SourceLocationat theUNKNOWNsentinel (uri="",line=0), whichDefinitionscollapsed to "no jump" through oneuri().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 sealedDefinitionTarget { Located, SourceAbsent, Ambiguous }(ingraphitron-lsp/.../definition/) replaces the sentinel;Definitionsresolves the service half through two pure FQN-join helpers (classTarget/methodTarget) and switches on the typed outcome exhaustively in one place (Definitions.resolve), whereSourceAbsentlogs a recoverable signal andAmbiguousis a deliberate silent no-jump.SourceWalker.Indexnow exposes theambiguousMethodsset the merge already computed and discarded, soAmbiguousis distinguishable fromSourceAbsent. Change 2, source-cadence index:CompletionData.ExternalReference/Methoddrop theirdefinitionfield; the LSP owns avolatile SourceWalker.IndexonWorkspace(sourceIndex()/setSourceIndex), seeded byDevMojoat startup and refreshed by a third watcher (sibling to the schema and classpath watchers) on.javaover the compile source roots, so a declaration that moves in a hand-edited source is jumpable without waiting for a.classrebuild.Definitionsjoins ref-from-catalog with position-from-source-index at request time on the FQN both carry.CatalogBuilderkeeps lifting Javadoc intodescriptionon the build cadence (hover untouched). Scope bounded to the service half (the reported bug); the jOOQ half, hover-description cadence, and the staticSourceWalker.CACHEare deliberate transitional states, each documented and deferred to the follow-up R352 (complete-lsp-position-decoupling). Coverage:DefinitionsTestpins eachDefinitionTargetarm reachable (Located/SourceAbsent/Ambiguous) plus the end-to-end class/method jumps;SourceWalkerTestpins theambiguousMethodsexposure;CatalogBuilderSourceTestasserts the build-cadence Javadoc lift now that positions are LSP-tier;CatalogRefreshTestpins a.javaedit refreshing the source index without a catalog rebuild; no code-string assertions on generated method bodies. Predecessors R90 (the source walk) and R351 (thecompileSourceRoots/classpathRootsparity 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 (theFilminfilms: [Film!]!, animplementsinterface, 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 theTypeDefinitionRegistryalready holds every type’s source position and ships to the LSP through the build snapshot. Producer:LspSchemaSnapshot.Builtgains a per-type declaration-location map (typeDefinitionLocations(), keyed by SDL type name) plus atypeDefinitionLocation(name)lookup, threaded through both leaf records' canonical constructors andWorkspace.demoteSnapshot; a new 5-arg convenience constructor on each ofCurrent/Previousdefaults the map empty so existing fixtures compile untouched.CatalogBuilder.buildSnapshotpopulates it fromregistry.types()+registry.scalars(), reducing graphql-java’s 1-basedSourceLocationto the 0-based coordinates every goto-definition consumer reads (mirroringSourceWalker’s `-1); null-source built-in scalars and the bundled-directive source are dropped rather than emitted as deadfile://URIs, the bundled source-name exposed asRewriteSchemaLoader.DIRECTIVES_SOURCE_NAME. Consumer:IntraSchemaDefinitions.computetakes the snapshot as an explicit parameter (test seam matching theDefinitions.computesibling convention, since the provider’s onlyWorkspace-snapshot install path demands fullBuildArtifacts); the open-buffer tree-sitter scan stays first and authoritative, falling back tobuilt.typeDefinitionLocation(typeName)on miss, andGraphitronTextDocumentServicepassesworkspace.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) andCatalogBuilderSnapshotTest(a producer-side arm over the realRewriteSchemaLoader.loadparse 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-basedsourceLocation(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@nodeIdreference on Graphitron-owned DML mutation inputs. A same-table@nodeIdcarrying 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 theNodeIdLeafResolver.resolvesame-table own-PK short-circuit on@referencebeing absent; with@referencepresent the leaf falls through toresolveFkJoinPath, which orients the self-FK withselfRefFkOnSource=trueand yields aResolved.FkTarget.DirectFkwhoseliftedSourceColumnsare 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 inresolve(), so the shape is admitted on the read side too (a same-table@nodeId @referencequery arg / filter resolves to aDirectFkself-FK filter,WHERE child_cols IN (decoded keys), no self-join, and the@asConnectionsame-table advisory correctly stops firing). D2 lifts the R315InputBeanResolver.buildRecordKeyDecodesame-table "self-reference out of scope" reject, routing the@servicejOOQ-record case through the sameBuildContext.resolveRecordFkTargetColumnsthe cross-table branch uses (orientedselfRefFkOnSource=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 +requireColumnAgreementcarry 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: thesame-table && !@reference ⇒ self-FKpredicate 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 existingreordered_fk_childfixture forcing a non-identity permutation through both reconciliations). Coverage: resolver-tierNodeIdLeafResolverTest(DirectFk landing + without-@referenceidentity contrast + two anti-drift cases),MutationDmlNodeIdClassificationTest(INSERT admits theCompositeColumnReferenceFieldover the self-FK child columns, surfacing the sharedmailbox_id),JooqRecordServiceParamPipelineTest(@serviceclassifier, replacing the dropped reject test),SelfFkNodeIdReferenceReadSidePipelineTest(read-sideDirectFkfilter + no-@asConnection-advisory), and execution-tierSelfFkNodeIdInsertExecutionTest(agree inserts / disagree throws + inserts nothing / omitted nullable leaves the lone decode, on theemail/mailboxfixture, 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@servicejOOQ-record path D3+D4;0535abb@mutationINSERT path D1/D2/D5;f739fd5single-row UPDATE SET D3 + UPDATE-path D2;0a9c8dfIn Review markup + R342 filing): Runtime value-agreement check for multiple@nodeIddecodes onto shared columns. When more than one writer (two@nodeIddecodes, a@nodeIddecode 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 predicateNodeIdEncoder.requireColumnAgreement(label, DataType, a, b): it coerces both sides through the destination column’s jOOQDataType(the same coercion the real write applies, riding the class-level@SuppressWarningsforDataType.convert, so format-variant wire values like"01"/1.0/BigInteger 1collapse onto the decoded1and agree, while a genuinelyvarchar"01"vs"1"still disagrees) and throwsGraphqlErrorExceptionon 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@serviceJooqRecordInstantiationEmitterprepare/agree/load emission, byte-identical when no column overlaps), D5 (the@mutationINSERT structural dedup inTypeFetcherGenerator.insertColumnPlandriving the column list plus a single coalesced typedField<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 (emitSetAgreementPreamblebefore theMap.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) andUpdateRowsWalkervia the newUpdateRowsError.PlainColumnCollision(UPDATE, single-row + bulk), the mutation mirror of the R336@servicereject. All checks are presence-guarded (an omitted nullable writer is not a writer and cannot conflict) and pairwise against the first present writer (equalstransitive). 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 bulkUPDATE … 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-tierNodeIdValueAgreementExecutionTest(the agree / disagree / presence-guard matrix across all three paths plus the format-variant"01"case pinning the coerced comparison, onstorage_bin/film_endorsementfixtures), pipeline-tierJooqRecordServiceParamPipelineTest(two identity decodes / plain-field-plus-decode admitted and deferred to runtime) andMutationDmlNodeIdClassificationTest(the two-plain-fields rejects on INSERT and UPDATE SET), and theRejectionSeverityCoverageTestaudit 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 (
21915b9exception + loader + DevMojo arm + tests;1bf9ca7self-review tightenings + R345 follow-on;63b033dIn Review): Surface schema parse failures as clean dev-watch diagnostics, not infrastructure stack traces. In thegraphitron:devwatch 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, becauseRewriteSchemaLoader.loadwrapped graphql-java’sInvalidSyntaxExceptionin a bareRuntimeExceptionandDevMojo.runGeneratorPasslogged that through the "infrastructure" armgetLog().error(..., e)(with the throwable). A new typedSchemaParseException(a sibling ofValidationFailedException, not a subtype, inno.sikt.graphitron.rewrite) is now thrown from the loader’sInvalidSyntaxExceptionarm only; theIOException/ missing-file arms stay bareRuntimeException(genuine infrastructure, keep their trace). ItsgetMessage()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 catchRuntimeExceptionand printgetMessage()) keep printing attribution unchanged; it also carries a nullableSourceLocation+briefconsumer-less for now, positioned to feed the deferred LSP-squiggle follow-on (R345).runGeneratorPassgains acatch (SchemaParseException)arm ordered before the generic infrastructure arm that logs the one-liner without the throwable and resetspreviousErrorKeys = null(a parse failure is not a validator verdict, so it must not feedWatchErrorFormatter’s delta tracker). `GraphQLRewriteGeneratoris unchanged: the exception propagates as-is throughloadAttributedRegistry()out of all three entry points with no translation step, so the one-shotvalidate/generatebuild still fails on a broken schema carrying the attributed message. Two reviewed deliberate deviations:runGeneratorPassrelaxed private → package-private as a test seam, and the consumer-lessbrieffield carried per spec for R345. Rejected fork (documented in the retired spec): routing throughValidationFailedException+ a fabricatedValidationError/Rejection.InvalidSchema, which would regress the quiet paths to a count string, falsifyValidationFailedException’s javadoc invariant, and stamp a pre-classification failure as a validator verdict. Coverage: unit `RewriteSchemaLoaderTest(throwsSchemaParseExceptionwith the offending file’sSourceLocation+ attributedgetMessage()pinned exactly to location + brief as the quiet-path regression guard; still bareRuntimeExceptionfor a missing file),SchemaParseExceptionPropagationTest(generate()propagates the same type, no translation), andDevMojoTest(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 toValidationFailedException. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R300 (
8249abbfixture + jOOQ-regen foundation;0074bd4model +@routinedirective +JooqCatalog.resolveTableValuedFunction+RoutineDirectiveResolver+ emitter;076cc56execution-tier proof +routine.adocdocs + fixture gates;b0533b1spec markup): First-class jOOQ routine support, day-one table-valued read slice. A new@routine(name:, argMapping:)directive backs a rootQueryfield with a jOOQ-generated table-valued function (PostgreSQLRETURNS TABLE/SETOF). jOOQ models such a function as a first-class catalogTable<R>, so the slice rides the existing@table-bound return-type and selection-narrowing ($fields) machinery unchanged; only theFROMsource differs, becoming a call to the schema’s globalRoutinesconvenience method with the routine’s IN parameters bound from GraphQL arguments. The provenance is carried by a newRoutineRef(the catalog-handle twin of@tableMethod’s `MethodRef) on a newQueryField.QueryRoutineTableFieldleaf, added toTypeFetcherGenerator.IMPLEMENTED_LEAVESso the four-way dispatch partition stays exhaustive;operation()isFetchandtarget()projects a bareTargetShape.Table, following theQueryTableMethodTableFieldprecedent. The deferred scalar-read and procedure-write forks reject at validate time viaJooqCatalog.resolveTableValuedFunction(they do not resolve as table-valued functions), satisfying "validator mirrors classifier" without aSTUBBED_VARIANTSentry, since no leaf is minted for them. Three reviewed deviations from the spec-as-reviewed: emission rides theRoutinesconvenience method rather than<ROUTINE>.call(...)(same SQL); catalog-based discovery with IN-param names depending on-parameterscompilation; classification reusesFieldClassification.QueryTableMethod(a dedicatedQueryRoutineis a follow-up). Coverage:ClassifiedCorpusroutine-table-valued-readfixture (Query / Fetch / List(Table)), a@ProjectionFor(QueryRoutineTableField)test inGraphitronSchemaBuilderTest, andRoutineFieldExecutionTestrunning 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-writeOperationwrite arm, scalar-function and record-returning reads, child-positioned@routine, heterogeneous binding sources, and the translation of legacy’s 26procedureCall*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 (
688a43cD1-D4 code + tests +CustomerRecordServicefixture; doc-fix rework13695d0): Flatten nested input-object fields in jOOQ-record@serviceparams. A@serviceparam typed as a generated jOOQTableRecordcan 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/RecordKeyDecodecarry an ordered, non-emptyList<String>access path (was a singlesdlFieldName) with aleaf()accessor; the last element is theMapkey, earlier elements the enclosing nested-input field names, and a top-level binding is a single-element path byte-identical to before (adopting theNestedInputFieldrepresentation R186 settled). D2:InputBeanResolver.buildJooqRecordrecurses into nested grouping inputs via a newcollectJooqBindings, threading the existingClassifyContextSDL-type-nameexpandingset for cycle detection, parallel to the member-axisbuildInputBeanwalk rather than routing throughclassifyInputField(a different carrier family on a different axis). D3: typedRejection`s through `JooqBuilt.Failfor 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:JooqRecordInstantiationEmitterwraps each multi-element binding in a null-safe parent-Mapdescent (theinstanceof Map<?,?>chain idiom fromArgCallEmittergeneralised to statement form) with collision-freecamelJoin-derived locals; an absent / null / non-Mapgroup 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-nullfield from a nested input-object value (both literal and variable wire paths) while retaining it on the top-level argument map, so a present-nullnested leaf is indistinguishable from omitted and leaves the column untouched; the top-level present-null→NULLthree-way narrows to a nested two-way, with no emitted-code change. Coverage: pipelineJooqRecordServiceParamPipelineTest(8 new cases: flatten with two-element paths, mixed top-level + nested, nested@nodeIddecode, depth-2, and the four D3 rejections by message substring) and 6GraphQLQueryTestexecution cases (lands-on-column + omitted sibling, present-nullcollapse 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-checkingcreateCustomerRecordagainst the real jOOQCustomerRecord; no generated-body string assertions. NewCustomerRecordServicefixture (CustomerRecordavoids thecreateFilmRecordper-record-class dedup clash onQueryFetchers). 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 (thePojoInput(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, animplementsinterface, or a union member) now jumps to that type’s canonicaltype 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. AddsIntraSchemaDefinitions(a provider in thedefinitionpackage parallel tohover/DeclarationHoversbesideHovers): it keys on the cursor sitting on anamed_typereference name outside any directive, skipsTypeNames.BUILTIN_SCALARS, walks open files via the per-URI lock-guardedWorkspace.getfast-skipping on the immutabledeclaredTypes(), and resolves through the newDeclarationKind.findDefinitionhelper (returns thenamenode of the canonical non-extension declaration, so navigation lands ontype Foo, notextend type Foo); the returnedLocationcarries the real declaration-name byte range viaPositions.toLspPosition, not the jOOQ path’s0:0placeholder. Wired afterDefinitions.computewith.or()inGraphitronTextDocumentService.definition(); the two paths key off disjoint syntax (anamed_typenever sits inside a directive argument) so they never contend. Coverage:IntraSchemaDefinitionTest(9 cases through a realWorkspace: same-file, cross-file,implementsinterface, union member, input field, built-in scalar empty, unknown type empty, cursor-on-declaration empty, definition-wins-over-extension; assertions on the returnedLocationURI +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+ 4b09f3868; slice 5e522fb4+ prose sweepc900455+code-generation-triggers.adoc3e3f607+ changelog forward-notes903e84d; In Review transition70ce94e): Pivot the field-dimensional model fromcarrier × intent × mappingto(source, operation, target). A field is an edge: it arrives into asource(a wrapper around aSourceShape, the arm being the arrival cardinality and the emit-strategy dispatch:Root|OnlyChild|Child), performs anoperation(a sealed interface with payload-carryingrecordarms replacing the flatIntentenum:Fetch/Paginate/Lookup/ServiceCall/Count/Facet/Nest/NodeResolve/EntityResolve/ the writes), and projects atarget(aSingle|Listwrapper around aTargetShape, 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:ServiceCallcollapses the formerQueryService/MutationServiceverb pair (read/write is theSource.Root.Query/Source.Root.Mutationlegality gate now), holding the two un-unified call carriers in a transitionalCallholder pinned to R314; the fusedMapping.TableConnectiondecomposes intoTarget.Single(Connection)(shape) +Operation.Paginate(windowed-read verb); declared-gap arms (Count/Facet/EntityResolve/UpdateMatching/DeleteMatching, plus theInterface/Unionparticipant payloads) are modeled-but-unpopulated with documented known-gap entries. Slices: 1 rewrote the R222 model (docs); 2-3 introducedsource()/operation()/target()additively (bridges deriving the retired axes so the corpus classified unchanged); 4a re-derivedOutputField.requiresReFetch()+ thedispatchPerformsReFetchvalidator mirror over the new axes (the bareTargetShape.Table× holds-records guard, behaviour-preserving againstmapping() != Mapping.Table); 4b migrated the@classifiedR281 corpus ontosource:/operation:/target:+sourceShape:/targetShape:, recutDimensionTupleto compare at the altitude the directive can express (Sourceby structural equality,Operationby arm type token,Targetby(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(thesourceWrapperIsTheFoldOfAncestorTargetWrappersinvariant, target half mirrored against the parsed SDL output wrapper, source half pinned at the conservativeChildstrength 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 namesIntent/SourceCardinalityabsent as whole words, carve-outsSourceShape/LookupMapping/MappingEntryretained);ClassifiedDslTest.everyDimensionValueIsExercised(the disjoint-exhaustive coverage partition over thesource/Operationseals with the SDL-vs-Java name mirror);ReFetchDerivationTestmigrated behaviourally onto the new axes;SourceShapeProjectionTestretained as the source-arm projection guard. No code-string assertions on generated method bodies. The recommendedleafReconstructsFromCoordinatecompleteness pin is deferred (the old flat-enum corpus never pinned payloads either, so nothing regressed). ThedispatchPerformsReFetchmirror survives R316 by design (retiring it is R314’s emit re-platforming). Downstream: theSourceKeydecomposition 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 inOutputField/ChildField, andReFetchDerivationTest’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 rework0d4acca): Bind FK-reference@nodeIdonto jOOQ-record@serviceparams. Generalizes R311’s same-table identity case to cross-table foreign-key references: a@serviceparameter typed as a generated jOOQTableRecordcan now be populated from an input whose@nodeIdfields 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 legacyNodeIdReferenceHelpers.mapKeyColumnsThroughForeignKeyinto the rewrite’s model. Model (D1):CallSiteExtraction.JooqRecord.keyDecode(Optional) becomeskeyDecodes(List, so a record may carry several@nodeIdfields);RecordKeyDecodegeneralizes R311’skeyColumnstotargetColumns(the resolved columns on this record the decoded values load into, identity or FK-child) and gains anonNullflag. Deliberately noKeyProjectionsub-axis: both arms loadtargetColumnsidentically, 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 ofBuildContext.synthesizeFkJoinintoresolveFkSlots, so the join path and the newresolveRecordFkTargetColumnsshare one bug-fixed orientation site (parent columns fromForeignKey.getKeyFields(), notgetKey().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):@tableon the input classifies it asTableInputType("Graphitron owns the DML"), which contradicts a jOOQ-record@serviceparam ("the service owns the DML");InputBeanResolverkeeps R311’sJooqTableRecordInputTypetrigger and adds a narrowerisTableRecordreject 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):JooqRecordInstantiationEmitterswitches from a singlefromArraybatch to per-binding conditional loads keyed onraw.containsKey(...), applied uniformly to@fieldcolumns and each@nodeIddecode: 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-nulltoNULLviaset(field, null)(reliable changed flag;fromArraynull-skips), and decodes-and-loads a present value; coercion stays on the non-deprecatedfromArraypath. Two R311 behavior changes carried, folded in deliberately (D4): (1) R311’s same-table identity singlefromArraybatch 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 disjointfromArraygroups" javadoc and theRecordKeyDecode"always throws … whetherID!orID`" 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@referenceon 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,@nodeIdwithouttypeName. The R311 single-@nodeIdgate is removed: multiple@nodeIdis 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: pipelineJooqRecordServiceParamPipelineTest(21 cases: FK deduction, the renamed-FK target columnendorsed_filmâ‰film_idreal pin, reordered composite-key decode-order reconciliation, explicit@reference(key:)disambiguation on the two-FKstudierett, the@tablereject, mixed identity + FK + plain@field, the full rejection set, the formertwoNodeIdFields_rejectflipped to a positive two-keyDecodesclassification) +SynthesizeFkJoinReorderedKeysTest(slot orientation pinned for both consumers); execution (sakila) the FK-child INSERT readback proving the decoded id lands on the renamedendorsed_filmchild column, the nullable FK-reference / nullable plain-column omitted-vs-null-vs-setchanged-flag contract, and (rework) the nullable same-table identity onAddressRecord(omitted → DB-assigned serial PK via the service-owned INSERT, set → decoded id on the PK), the only tier that observeschanged=falseexclusion; compile (graphitron-sakila-example)createFilmEndorsementRecordagainst real jOOQ at Java 17. Adds thepublic.film_endorsementrenamed-FK fixture (FK childendorsed_film≠referencedfilm_id) and reusesidreffixture.studierett/nodeidfixture.reordered_fk_child/child_ref. User docs: FK-reference@nodeIdon@servicejOOQ-record params documented innodeId.adoc. Generalizes R311; the@table-on-input deprecation /argMappinggrouping stays R97, self-reference and pojo member-axis FK-@nodeIdare separate items, overlap value-agreement is R322. Motivated by fourutdanningsregisteret-graphql-specstatus-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 theSchemaTraverservar 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/@keyseeds, 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: theNodeIndexstays precomputed and threaded in, reflection grounding becomes on-demand (noRecordBindingResolver.resolveAll()precondition). R317 stays backwards-compatible: it restructures type classification and keeps the leafChildFieldmodel 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 as1ac9c75; the two discards reconciled here.) Number retired, not reused. -
R324 (
45bf3cb): Lift the single-cardinality multi-hop@splitQueryrestriction. A single-cardinality@splitQuerychild field (returnsT, not[T!]) whose@reference(path:)had more than one hop was rejected at classification time (FieldBuilder.classifyObjectReturnChildField’s `FieldWrapper.Single && elements().size() != 1Rejection.deferred), even though the supporting classification machinery (deriveSplitQuerySourcekeying offpath.get(0),BuildContext.buildParentCorrelationpinning onlyfirstHop) was already hop-count agnostic. The gap was entirely inSplitRowsMethodEmitter: of the three cardinality siblings onlybuildSingleMethodnever grew the bridging-hop loop, projecting/FROMing offfirstAliaswith 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 +OnConditionJoinparent JOIN +parentInputcorrelation) andbuildWhereCondition(per-hop FKwhereFilter`s + field-level filters); all three siblings route through both, and the connection WHERE loop’s unconditional `(JoinStep.FkJoin)cast becomes the sharedinstanceof-guarded form.buildSingleMethodnow projects/FROMs offterminalAliasand bridges multi-hop, returningscatterSingleByIdxunchanged; single-hop paths collapse the bridging loop to a no-op, so theRecordTableFieldemitsSingleRecordPerKey()path that also routes throughbuildSingleMethodemits exactly as before (audited, no change required). Bridging hops are inner joins consistent with the siblings: a to-one chain resolves tonullwhen any hop is absent; distinguishing intermediate-null from terminal-null (LEFT JOINs) stays out of scope. The classifier guard is removed; the motivatingCustomer.storeAddressshape (customer -> store -> address) now classifies asSplitTableFieldwith a 2-hopjoinPath. Coverage: pipelineGraphitronSchemaBuilderTest.SPLIT_TABLE_MULTI_HOP_SINGLE_CARDINALITY(the former_REJECTEDenum, now a positive 2-hopFkJoinassertion keyed onstore_id), executionGraphQLQueryTest.splitTableField_singleCardinality_multiHop_bridgesToTerminalAddressPerCustomer+_dedupesSharedKey_oneBatchRoundTrip(correctAddressper customer matching the inlinestoreAddressnavigation, and a single batched rows-method round-trip); the null-where-no-match semantic is structurally covered byScatterSingleByIdxTest+ the existing single-hop null-FK fixtures since Sakila’scustomer.store_id/store.address_idare both NOT NULL. The now-false "single-cardinality multi-hop requires split" bullet is dropped fromsplit-vs-inline.adoc(the shape works inline and via split). Emitter-only; no model-shape, directive, or wire-format change. Motivated by asis-graphql-specconsumer shape. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db). -
R23 (
c38779e): Multi-parentNestingFieldsharing,TableFieldarm. Lifted the multi-parent shared-shape gate inGraphitronSchemaValidator.compareNestedFieldsShapeforChildField.TableField: a plain-objectNestingFieldtype may now be shared across multiple@tableparents when its shared leaves include inlineTableField`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 agetClass()mismatch) already guarantees both sides areTableField, andreturnType()derives from the single SDL declaration on the shared nested type so it is identical by construction; per-parentjoinPath/filters/orderBy/paginationare intentionally not compared because each parent’s$fieldsemits its own correlatedDSL.multisetarm. No emitter or wiring change:TableFieldis aPROJECTED_LEAFwhose reified read (FetcherEmitter.bind, wrapped inLightFetcher) pulls by field name from the sourceRecordwithout consulting the outer parent table, so first-parent-wins nested-type registration has no runtime effect for this leaf. Coverage: pipelineGraphitronSchemaBuilderTest#multiParentSharedNesting_inlineTableFieldLeaf_classifiesAndValidatesPerParent(two@tableparents sharing a nested type whoseaddressleaf classifies as aTableFieldwith a distinct per-parent FKjoinPath; validator emits no error), executionGraphQLQueryTest#multiParentSharedNesting_inlineTableField_returnsAddressPerParent(sharedOccupantLocationacross Customer and Store, each resolvingaddressper parent, the Customer side pinned to the same row as the directCustomer.addressFK navigation), and the existingNestingFieldValidationTestnon-TableFieldrejection stays green. Retired the stale#8roadmap pointer in the catch-all comment; filed the BatchKey-leaves follow-up (and the openLookupTableFieldre-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 renderValidationFailedException.errors()in the Maven failure output, at parity withDevMojoand theSchemaProblembranch.AbstractRewriteMojo.runGeneratorpreviously special-casedSchemaProblembut letValidationFailedExceptionfall through to the genericcatch (RuntimeException e)arm, which rethrew only the exception’s message (the bareN schema validation error(s)count) so a consumer build (opptak-subgraph) saw nofile:line:coldetail. The fix adds a siblingcatch (ValidationFailedException e)arm ahead of the generic one, mirroring theSchemaProblemarm: it wraps the cause in a null-message intermediary (so Maven’sDefaultExceptionHandlerdoes 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-privatevalidationFailureMessage(List<ValidationError>)that prepends a"GraphQL schema validation failed:"header (matching theSchemaProblemDiagnosticarm) toWatchErrorFormatter.format(errors, null), the same renderer thegraphitron:devloop uses (nullprevious-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(), theGraphitronSchemaBuilder.buildBundlefederation-recipe rewrap, orTagLinkSynthesiser.apply). Coverage:AbstractRewriteMojoTestasserts the message carries per-errorfile:line:coldetail (not just the count) and embeds the exact treeWatchErrorFormatter.format(errors, null)produces (structural dev-loop parity), at the same formatter-level tier as the siblingSchemaProblemDiagnosticTest. 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 4507242f, slice 54a1a117, slice 6 prune0e13c12+ verb-collapse2437a84; In Review transitiondf6c1f7; 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/@keydirective scan, and descends output edges (field→target, union→members native; interface→implementor and object/interface→interface via the customSchemaTraverserchild function, the load-bearing fan-out that keeps a directly-seeded federation implementor from pruning theNodeinterface itsimplementsclause references). The accumulator owns reconciliation:TypeRegistry.registeris the sole write verb (the Q2 verb-collapse), reconciling repeated registrations three ways (equal → idempotent, compatible → merge incl. the cross-carrier federation@tagunion andshareableOR for synthesised Connection/Edge/PageInfo, incompatible → demote toUnclassifiedType); the formerclassify/enrich/synthesize/demoteverbs dissolve into it, the traceOpderived from the reconciliation arm so per-call observability survives. The classifier is a pure producer reading only node SDL + reflection + downwardTraverserContextcontext (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 sidewaysctx.types.getread); the field-first driver inversion with compensating orphan sweeps (3b);DomainReturnTypeenforcement relocated from a reclassifying post-pass to aGraphitronSchemaValidatorrule (collectDomainReturnTypeConflicts+validateUniformDomainReturnType, model change + validator rule in one commit, no enforcement gap);ConnectionPromoterfolded into the walk assynthesiseForFieldwith a single-producerrebuildAssembledForConnections(5); orphan prune made observable (an unreachable@tableobject 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 deletingTypeBuilder.buildTypes) is deferred to R317 (now Ready); the driver is field-first and the walk is the sole classifier, butbuildTypesstill hosts the type-classify loop over the walk’s reachable set. Coverage:SchemaReachabilityTest(thereachable ⊆ classifiedsafety 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: theGraphitronSchemaBuilderTestexhaustive truth table + sakila pipelineTypeSpec+ the Java-17graphitron-sakila-examplecompile + 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/FieldClassificationprojections 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 jOOQTableRecord(singularRecordorList<…>) directly as a@serviceinput param. A top-level@serviceparameter whose Java type is a generated jOOQTableRecordalready classified asJooqTableRecordInputTypeon the type side but could not be bound at the call site:InputBeanResolver.enrichbean-ified it on the Java-member axis, so a@fieldnaming a column matched nothing and the build rejected with a misleading "has no fields matching".enrichnow 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@fieldfield resolves to aColumnRef(aCallSiteExtraction.ColumnBinding), and a single@nodeIdfield decodes the record’s scalar key (R195’s wire mechanism projected onto the param record’s own identity, aRecordKeyDecode); a lifted record-type-mismatch gate rejects a foreign-table@nodeId. New model: theCallSiteExtraction.JooqRecordpermit + the column-axisColumnBinding/RecordKeyDecoderecords (siblings to the member-axisInputBean/FieldBinding, each carrying its ownsdlFieldNameMap key since neither rides aFieldBinding), with a compact-constructor at-least-one-binding floor; andValueShape.JooqRecordInput, a path-carrying leaf that also carries its construction carrier (theScalar-carries-leafTransformprecedent) so the helper-queue collector registers from theValueShapealone. A newJooqRecordInstantiationEmitteremits a dedupedcreate<Record>(Map)/create<Record>List(Object)helper pair (recordfromArray(…, Tables.<T>.<col>)for the columns, no deprecatedDataType.convert, +NodeIdEncoder.decodeValuesfor the identity), reached identically from the root emitter (ServiceMethodCallEmittervia the new leaf) and the child-coordinate emitter (ArgCallEmitterreal arm) ; the binding is coordinate-agnostic (enrichruns for child@servicetoo, before theisRootgate) and both cardinalities share one construction site (the plural maps the singular per element). TheTypeFetcherGeneratordual 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 aJooqRecord(Input)is never anInputBeanfield leaf. The misleading bean message becomes honest, validate-timeUnclassifiedFieldrejections (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), fourGraphQLQueryTestexecution 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 childArgCallEmittercall against the real catalog; no generated-body string assertions. A call-site param-binding NOTE lands on the@servicesurface incode-generation-triggers.adoc(the classification was already documented). Out of scope:Set<TableRecord>(inherits theInputBeanpath’s imperfectSethandling), FK-reference@nodeIdand@table-on-input (both R97). Motivated by theendreUtdanningsspesifikasjonsstatus(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 newBuildContext.diagnoseForbiddenCarrierDirectivewould-admit-but-for-the-directive probe re-runs the structural DML scan under a privateForbiddenDirectivePolicy.IGNOREgate, leaving the public scan contract (scanStructuralDmlPayload/scanStructuralServiceCarrierPayloadand every speculative caller) byte-for-byte unchanged; when the payload would admit as a carrier but for the forbidden directive, the singleScalarReturnTypearm ofMutationInputResolver.validateReturnType(where all three DML kinds converge) surfaces a targeted message naming the data field and the@-prefixed directive, with the@service-carrier@splitQueryasymmetry note (R275, thewarnIfSplitQueryOnRecordParentadvisory) appended conditionally. The generic message stays the fall-through for genuinely unsupported scalar returns. The rejection keeps the uniform proseRejection.structuralshape of its siblingvalidateReturnTypearms; a typedReturnTypeErrorsub-seal lift is deferred. Coverage: fourMutationDmlCasepipeline cases (UPDATE + INSERT@splitQuerythrough 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 onUnclassifiedField.reason()with no generated-body string assertions. Discovered during theutdanningsregisteretGraphitron 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 2355f9b5, slice 3 collapse7e672b9+ re-fetch derivationd829d42, source-shape mirror18d3aee; In Review transitionsc6847c6+18d3aee): Forward note (R316, 2026-06-19): thecarrier/intent/mapping/ source-cardinality model this entry describes was pivoted to(source, operation, target); theCarrier/Intent/Mapping/SourceCardinalitytypes 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 collapseSingleRecordTableFieldintoRecordTableField. R222 Stage 3 follow-on to R290. The carrier dimension gains aSource-arm source-shape (Table | Record, the input-side mirror ofmapping’s `Table:Column :: Record:Field) and source cardinality (One | Many);Carrieris now sealed (Query/Mutationpayload-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 derivedRecord-to-Tablecrossing.OutputField.requiresReFetch()derivesTable 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.dispatchPerformsReFetchmirrors it.SingleRecordTableFieldis deleted: the twoFieldBuildercarrier sites (R178 DML, R275@service) now buildRecordTableFieldviabuildPayloadCarrierRecordTableField(a singleJoinStep.LiftedHopover the target PK folding source=target onto oneColumnRef, plus a newSourceKey.Reader.ProducedRecordRead); intent staysFetch. 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-rowsVALUES(idx,pk) JOIN … ORDER BY idxscatter; only the key reader differs. Source cardinality is conservatively hard-codedMany(the absorbing element, always-correct as a one-element batch); the inlineOne-skip optimisation is kept as dead code until R279 computes the true ancestor-product cardinality.OrderingOwnedByProduceris deleted, itsvalidateListRequiresOrderingexemption replaced by a plainrequiresReFetchexemption (also fixing a latent bug: a PK-less idx-ordered re-fetch is now admitted rather than wrongly rejected). The 352-lineFetcherEmitterSRTF path is removed with the LocalContext null-source guard preserved; the LSPFieldClassification.SingleRecordTableprojection collapsed intoRecordTableTarget. One honestly-documented divergence: dispatch routes by leaf identity (everyRecordTableFieldbatches) rather than literally reading theManyslot at the fork; net effect matches the spec, R314 filed for the follow-up. Coverage:ReFetchDerivationTest(trueacross the Record-source family + mirror agreement),SourceShapeProjectionTest(walks every corpus-demonstratedChildFieldand cross-checkssourceShape()against the parent type’s independently-classified backing ;TableBackedType→Table, elseRecord; exercising both arms, with a reflective sealed-leaf guard overChildFieldwhose uncovered leaves must carry a documentedNOT_CORPUS_COVEREDentry; the validator-mirrors-classifier analogue ofdispatchPerformsReFetch), the@classifiedcorpus grown withsourceShape/sourceCardinalityargs and the former-SRTF row retargeted toRecordTableField/Source{Record, Many}/ intentFetch,SingleRecordPayloadPipelineTestinstanceofretargets,GeneratorCoverageTest.everyGraphitronFieldLeafHasAKnownDispatchStatusexhaustive/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-orderedVALUES-join scatter and its re-fetch verdict flips totrue, while the runtime result (same rows, same source order) is preserved. Out of scope: the@servicecarrier arriving as a list (R308), target-cardinality-many on theOnepath, and the broaderSourceKey.Cardinalitywrapper().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 inQueryViewRendereroutput: above a field it describes the field, above... on Tor a top-levelfragment f on Tit describes typeT; multiple comment lines join into a block-string"""..."""description, and a comment-free projection renders unchanged (the existing sevenQueryViewRendererTestcases stay byte-equal).Touchedgains type/field description side tables,Walkrecords them through the singledescriptionOf(Node)source seam (where native executablegetDescription()reads onFragmentDefinition/VariableDefinitionfold in once graphql-java is bumped past the pinned 25.0), andprune/keptFields/stripInternalDirectivesstamp them onto the rebuiltDescribedNode`s via a shared `applyDescriptionhelper.Fieldprose stays comment-sourced becauseFieldis not aDescribedNodein any graphql-java version, making comments the durable carrier rather than a stopgap. This is test-and-docs tooling undersrc/test, not the production generator path: thecatalogcorpus example’squery()gains comments so its rendered block oncode-generation-triggers.adoccarries field descriptions, andClassifiedDocTestguards that block verbatim. Coverage: five newQueryViewRendererTestpipeline 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 (thedescriptionOfextension), descriptions on production generator output, and operation-level (query { ... }) descriptions. Full reactor green. -
R200 (
e7be7f4): Honor@field(name:)inInputBeanResolverfor@serviceinput-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 houseargString(f, DIR_FIELD, ARG_NAME).orElse(f.getName())idiom, the input-side mirror of R191’s output accessor axis), andbuildInputBeanBodyis restructured: the record-vs-JavaBean target is computed once and dispatched viaswitch (target)intobindRecord/bindJavaBean(the mid-loopisRecord()re-tests collapse;recordOrder/sdlOrderdeleted); a single collision-checkedsdlByBindingKeyindex 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 inbindRecord(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 sharedbindFieldreturning a builder-internal sealedFieldResult(Ok/Fail); no newCallSiteExtractionleaf, soInputBeanInstantiationEmitter’s exhaustive switch is untouched, and `FieldBindingkeepssdlFieldName(the wire/Mapkey) separate fromjavaFieldName(the member) so emit stays selection-agnostic. Rejections ride the existingBuilt.Fail→Rejection.structural→UnclassifiedFieldpath. The@fielddocstring 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 R97org.jooq.*looksLikeBeanCandidateseam (jOOQ-TableRecord-as-param) and the R195@nodeIdjOOQ-record-member leaf are untouched. Coverage: pipeline-tier positive cases (record + JavaBean renamed via@field, assertingFieldBinding.javaFieldName()is the directive value whilesdlFieldName()stays the SDL name), rejection cases (direction A/B, ambiguity, blank value, all onUnclassifiedField.reason()), a regression floor (a divergent-name JavaBean without@fieldstill rejects "has no fields matching"); execution-tiersubmitFilmReviewSummary_routesThroughFieldRenamedRecordBeanround-trips through a@field-renamed record bean (the round-tripped reviewId proves positional binding); new fixturesTestInputBeanRenamed/TestInputJavaBeanRenamed/TestInputSubsetRecord/FilmReviewSummary. Input-side counterpart of R191; R201 / R202 carry the remaining@field-symmetry items. Full reactor green. -
R307 (Part A
2741787+ Part B5fe85f5; In Review transitioncce19f3): Retire stale@recordreferences.@recordstays a declared, legal-but-ignored directive (directives.graphqlsdeclaration +readRecordClassNameintact, 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 standaloneTypeBuilder.emitDirectiveIgnoredWarningspost-classification re-walk is replaced by a per-typeemitDirectiveIgnoredWarningcalled 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@recordas a liveExternalCodeReference-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 legacyname:→className:alias nudge. Because theInputField("ExternalCodeReference", "className")coordinate is shared with@enum, each surface gates on the enclosing directive name (mirroringMETHOD_VALIDATING_DIRECTIVES); the completion site gained the directive via a newdirectiveNamefield threaded fromLspVocabulary.CursorLocationintoCompletionContext. No@deprecated/deprecatedCoordinateswiring; the editor’s "ignored" signal stays the generatorBuildWarningalready surfaced throughDiagnostics.validatorDiagnostics. (3) Every applied@recordwas purged from test-fixture SDL in both modules: generator binding-hint drops are classification-neutral (reflection binds via the@service/@mutationproducer or@table), warning coverage consolidates into the newRecordDirectiveIgnoredWarningTest(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@recordto assert the absence of tooling, andValidatorDiagnosticsTestpins the@record-ignoredBuildWarningsurfacing as a usage-siteWarning.BuildOutputReportPipelineTest’s report-wiring warning swapped from a redundant `@recordto a redundant@splitQuery(the test needs only a model warning). The only applied@recordleft in either test tree isRecordDirectiveIgnoredWarningTestand the LSP carve-out fixtures. Out of scope (deferred): removing thedirectives.graphqlsdeclaration. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R264 (
98bafe8; In Review transitionef7bd17): roadmap-toolstatusround-trip no longer strips quotes from front-matter titles. Thestatussubcommand rewrote the block through a snakeyamlload-then-hand-serialize round-trip with no value-quoting, so a quotedtitle: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, threwScannerExceptionand left the file unreadable. Replaced the lossy write path withpatchFrontMatter, 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 siblingwriteChangelogNextIdthrough the same helper, retiring its identical hand-serialization loop. Coverage:RoadmapTitleQuoteRoundTripTestruns a fullstatussubcommand (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 directpatchFrontMatterunit test for present-key replacement and absent-key append. -
R290 (
5ebc52cslice 1 +1227f0cslice 2 +84a49f2slice 4; In Review transition38006ad; docs passes84de71e+6ceafb0): Forward note (R316, 2026-06-19): thecarrier × intent × mappingmodel this entry describes was pivoted to(source, operation, target); theCarrier/Intent/Mappingtypes and thecarrier()/intent()/mapping()accessors named below no longer exist. Read the dimensional vocabulary here as historical. Field-side dimensional slots, materialisecarrier × intent × mappingon 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 onOutputField(the field root that survives Stage 6), computed at classification time, reproducing exactly what R281’s throwawayLeafTupleAdapterreconstructed; the dimension enumsCarrier/Intent/Mappingmove into themodelpackage, the adapter is deleted, and the classified-corpus harness builds its test-sideDimensionTupleby reading the three accessors off the field (ClassifiedHarnessline ~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 readscarrier, polarity reads theintentfamily, build-vs-consume readsmapping, re-fetch readsintent × mappingjointly). The triple is a total classification (every field has all three), so noNo<Family>absence arm. Slice 2:ConstructorFielddissolved as wrong-by-design, a@tableparent constructing a@record/@servicechild from its own row, reachable only via self-referential test coverage. The classifier’sResultTypearm inFieldBuilder.classifyChildFieldOnTableTypenow rejects with anUnclassifiedFieldwhose structural rejectionGraphitronSchemaValidatorsurfaces as a build-time error; the leaf, its dispatch (IMPLEMENTED_LEAVES/FetcherEmitter/TypeFetcherGenerator/CatalogBuilder), and itsLeafTupleAdapterarm are removed. Theconstructorcorpus example leaves the classified corpus and becomesConstructorFieldValidationTest’s rejection fixture; the `GraphitronSchemaBuilderTestverdict +@ProjectionForsibling are deleted and theSingleRecordPayloadPipelineTest/DummyFetcherFixturesconstructor-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 →@tablere-query predicate, derived fromintent × mapping(mapping == TableAND intent in{QueryService, MutationService, Insert, Update, Upsert, Delete}) rather than re-decided per leaf in the consumer;GraphitronSchemaValidator.validateFieldmirrors 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 (theSingleRecordTableField→RecordTableFieldcollapse) 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). TheChildField→SourceFieldcarrier rename is split to R302. Coverage:ReFetchDerivationTest(behavioural assertions on the accessor and on validation output, nocode().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 removedconstructorexample), and the compile + execution tiers against real PostgreSQL as the behavioural backstop. Full reactor green end-to-end. -
R303 (
fbe9ac6+642d67a+3462bf8; docs-hygiene rework6adebd5+7f98c68): Reify inline datafetchers into named<Type>Fetchersmethods. Most generatedDataFetcher`s were emitted as anonymous inline value expressions in `<Type>Type.registerFetchers(lambdas, the R244/R268 arm-switch ternary, the record-walking blocks) or barenew ColumnFetcher<>(column)instantiations, leaving a datafetcher with no named symbol to breakpoint, stack-trace, or look up by field. Now every datafetcher is apublic staticmethod on the corresponding<Type>Fetchersclass 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 sealedFetcherBinding(Inline|Reified) so the field-name-to-method-name derivation lives in one place and the registration value and method declaration cannot drift;bindreturnsReifiedcarrying both theMethodSpecand the registration value (a bareFetchers::fieldfor env-dependent reads, ornew LightFetcher<>(Fetchers::field)for source-only reads), andTypeFetcherGenerator.generateTypeSpeccollects the reified method alongside the existing variant switch (method-backed variants returnInline, so no double-emission; the dispatch partition is untouched). The light path is preserved by renaming the generatedColumnFetcher→LightFetcher: it holds aRead<T>source-read SAM (T apply(Object source)) instead of a jOOQField<T>, stays aLightDataFetcher, 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). TheBatchKeyField-only gate for nested-type fetcher classes widens to "owns any fetcher" via one sharedFetcherEmitter.nestedTypeOwnsFetcherspredicate 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>Fetchersdelegate classes (ConnectionFetcherClassGenerator) whose thin per-field methods forward to the sharedConnectionHelper(one home for the pagination logic, hand-auditable;totalCountkeeps its SDL-presence gate);@errortypes get<ErrorType>Fetcherswith reifiedpath/messagereads (ErrorTypeFetcherClassGenerator) wired in place of the inline cast-lambdas inGraphitronSchemaClassGenerator. The R244/R268Outcomearm-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@errorPayloadAccessorerrors field staysInline(PropertyDataFetcher.fetching(name))because reifying it needs a generation-time resolved accessor thatChildField.ErrorsField/Transport.PayloadAccessordo not carry, a classifier change this Spec scoped out; R304 (filed Backlog) carries the classifier-backed reification plus theresolvesViaPropertyDataFetcher/validateOutcomeChildArmSwitchreconciliation and theDataFetcherKind.PROPERTY_FETCHERretirement. Behaviour-preserving relocation: no classifier branch, no validator-mirror consequence. Coverage:FetcherPipelineTestwiring-kind + method-presence assertions (propertyField_onRecordType_reifiesReadMethod,propertyField_onBackedRecord_wrapsAccessorReadInLightFetcher,recordField_onRecordType_reifiesReadMethod,outcomePayload_columnDataField_armSwitchesInlineReadOnSuccessValueflipped toCOLUMN_FETCHER-wrapping-a-method-reference), the per-type "class is emitted" pins for<Conn>/<Edge>/<ErrorType>/no-BatchKeyField-nested classes,TypeSpecAssertions.wiringFormatchingnew LightFetcher, and the compile-spec (sakila-example,<release>17</release>) + execute-spec tiers as the structural and behavioural backstops; noCodeBlock-string-equality assertions on reified method bodies. TwoIn Review -> Readycycles of docs-hygiene rework swept the staleColumnFetcherspelling and the inverted "no per-field fetcher method / emitted inline" invariant out ofsrc/mainjavadoc per "Documentation names only live tests/code". Full reactor green end-to-end. -
R284 (
1e2e719In 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@referenceConditionJoinemission. An FK-first-hop-then-@conditionbridging path emitted the two-arg condition-method call as(targetAlias, sourceAlias), violating R16’s fixed(srcAlias, tgtAlias)convention; with the documented opptaksamordnaOrganisasjonershape (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, andSplitRowsMethodEmitter’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 genericTable<?>parameters, which compile either way. Guard: the newReferencePathConditionFixtures.filmActorJunctionToActor(FilmActor, Actor)fixture takes concrete incompatible types, so any future re-reversal fails to compile in compile-spec. Coverage: executionGraphQLQueryTest.splitTableField_bridgingConditionJoin_returnsActorsPerFilmround-tripsFilm.actorsViaJunctionCondition(split-rows path); inline-lookup guardFilmInlineBundle.actorsByKeyViaJunctionConditionroutes the same FK-then-bridging-@conditionpath throughInlineLookupTableFieldEmitter(the fifth site). Full reactor green. -
e6d213d(reframe) +cc18815(impl) →5e34fb7(Spec → Ready) ; R299 (intention-classification-dimension): Forward note (R316, 2026-06-19): thecarrier x intent x mappingmodel this entry migrated the corpus onto was itself pivoted to(source, operation, target); the@classifieddirective andDimensionTuplenow carry the new axes. Read the vocabulary here as historical. migrate the R281 corpus from the two-axis(producer, mapping)verdict onto R222’s refinedcarrier x intent x mappingmodel, while the leaves are still intact and ahead of R290’s field-side materialisation.DimensionTuplebecomes(carrier, intent, mapping);ProducerStepretires; newCarrier {Query, Mutation, Source}and the full-modelIntentenum land, mirrored SDL-side inClassifiedDsl.PRELUDEand checked bycarrierMirrorsAdapterValues/intentMirrorsAdapterValues.LeafTupleAdapterreconstructs all three from leaf identity (carrier from the enclosing sealed type, intent from leaf +DmlKind, mapping as before); the switch stays exhaustive overOutputField, and the derived layer (FetchRelated/ re-fetch / new-query / polarity) stays computed, never asserted.@classifiedmigrates to(carrier:, intent:, mapping:)across every corpus fixture;everyDimensionValueIsExercisednow coversCarrier+Intentwith a known-gap allowlist (the five R222 model-completeness gaps plus upstream-rejectedUpsert, mirroringNO_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 fullmvn install -Plocal-dbis warning-free under-Xlint:all -Werror, leaving only declared-out-of-scope environment lines (sandbox jOOQ PG-version mismatch, Maven’s own Guice/UnsafeJVM notes, the R294BuildWarning-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.propertiestest-jar exclusion) plus emitter fixes. The generated-code casts the spec slotted for narrowest-scope@SuppressWarningswere instead dropped:env.getArgument/env.getSourceare<T> T, so a typed-LHS statement removes the cast via inference (the spec’s preferred step-1 over its mis-categorised example);@SuppressWarningsreserved for the genuinely-unchecked residuals ((List<X>) map.get(key)offMap<?,?>).cf8262efurther replaced the record-carrierOutcome.Success<?>capture + uncheckedsuccess.value()cast with a checkedinstanceof Outcome.Success<element>pattern-match (Success<T> implements Outcome<T>), via a sharedemitRecordSourceLocalhelper. Guard:-Werroradded to the parent pom’s globalcompilerArgs(every-Xlint:allcategory 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 thepublic.*codegen (no catalog consumer navigates those to-many path methods). -
2228b67…aa7a45e; R281 (classification-test-dsl): classification test DSL,@classifiedspec-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 (enumsProducerStep/Mapping/TypeVerdictvalidated SDL-side viaClassifiedDsl.PRELUDE, never leaked into the productiondirectives.graphqls), assert the two-axis dimensional verdict R222’s field pivot will adopt:producer(a pipeline of length ≤ 2:∅inline-correlate, or stepsQuery/Service/Dml) ×mapping(Table/TableConnection/Column/Record/Field). The throwawayLeafTupleAdapterbridges today’s fused sealed leaves to those tuples via a compiler-exhaustive switch overOutputFieldthat is R164’s leaf↔dimension truth table.ClassifiedHarnessclassifies each fixture with today’s classifier and compares;ClassifiedDslTestpins three coverage obligations (adapter totality compiler-enforced, every dimension value exercised,TypeVerdictmirrorsGraphitronType’s non-failure leaves with a simple-name-uniqueness guard). `VariantCoverageTestwas rewired so output-field and non-failureGraphitronTypeleaves are owned byClassifiedCorpus.coveredLeaves()as the single source of truth, while input-field leaves stay on theGraphitronSchemaBuilderTestenum table and the failure leaves stay out of scope.QueryViewRendererrenders 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.adocrenders its worked examples from the corpus (ClassifiedDocTest) with reference tables corrected againstTypeFetcherGenerator’s four-way emission partition (only `CompositeColumnReferenceFielddeferred). Retirement inventory committed atroadmap/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; theTableInterfaceField/TableMethodFieldper-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@tagapplications of their@asConnectioncarrier field, closing the contract-composition break where a tag-filtered contract kept the carrier field but dropped its untagged return type.ConnectionPromotercollects 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 existingshareablearm, and folds a tag union across all promoted carriers into the synthesised PageInfo exactly aspageInfoShareablefoldsshareable; an author-declared PageInfo is left untouched. Carriers sharing oneconnectionName:union their tags into the already-registered entry viatypeRegistry.enrichon a transformedschemaType()(no paralleltagsrecord component, per Model metadata over parallel type systems). Tests:ConnectionPromoterTest(explicit, repeatable, shared-name union, structural arm, SDL-PageInfo negative pin) +ConnectionFederationTagPipelineTest(<schemaInput tag>vialoadAttributedRegistryand a federation-SDL emission round-trip). The deferredshareableboolean 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 tests24387b2, comment refresh905f9ef) ; R275 (source-record-carrier-service-error-channel): error channel and data projection for source-record-carrier@servicemutations, reopened-scope completion. The earlier as-built slice (2026-06-05) closed only the to-one, non-@splitQuerycarrier ({ entity: Table, errors }projected offOutcome.Success.value(), bucket Cerrors: nullon the success arm, theNonNullableErrorsFieldrejection); this completion covers the two data-field shapes theopptak-subgraphsaksbehandling mutations actually use, both of which previously emitted an invalid assembled schema (atypeRefto 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 tolerantBuildContext.scanStructuralServiceCarrierPayload(the data field’s PK-keyed follow-up SELECT makes@splitQueryredundant, fired as the establishedwarnIfSplitQueryOnRecordParentadvisory), classifyingSingleRecordTableFieldMANY over theOUTCOME_SUCCESSenvelope; and a recognized-but-unbound orphan carrier becomes a loudUnclassifiedFieldat 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 offOutcome.Success.value()’s in-memory record(s) with no follow-up SELECT, deletion-safe by construction; structural scan grew a named `CarrierFamilyaxis (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 lockstepresolveDeleteIdEncoder+classifyDeleteIdEncoderErrorpair collapsed into one sealedIdEncoderResolutionresolver feeding both DELETE and SERVICE diagnostics, and a new sealed leafChildField.SingleRecordIdField(wired through every sealed-coverage site: validator,TypeFetcherGenerator.IMPLEMENTED_LEAVES,FieldClassification.SingleRecordId,LeafTupleAdapter, LSP hover/label). Slice 3 (77573c4, requirement 1): the shape-agnosticGraphitronSchemaBuilder.rejectDanglingTypeReferencesbuilder pass demotes any classified field whose SDL Object return element never registered toUnclassifiedField(fails the build and removes the field from emission), closing the residual hole for errors-only and scan-Rejectorphans that the per-shape guard left open; the seven historically-lax arg-mapping fixtures were fixed (not allowlisted) by backingFilmDetailswith a realTestFilmDetailsDto. Coverage: pipelineSingleRecordTableFieldServiceProducerPipelineTest, schema-builderGraphitronSchemaBuilderTestRootFieldCase+UnclassifiedFieldCaserows, executionGraphQLQueryTestdelete-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 session017DpiWem9o8HCVkgDf7ae5a): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen 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 fromdirectives.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 typedAuthorError. The retention decision isschema.types()membership, consumed by both the runtime registration and theSchemaSdlEmitterprint seam. Status flip and R253 closure at80623b0. -
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 theSchemaSdlEmitterprint seam (survivor-filtered directive definitions/applications on both arms,generateServiceSDLV2-mirroring federation printer) and re-enabledFederationBuildSmokeTest.emittedSdlMatchesRuntimeSchemaas 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@tableinput is now admitted on@mutationfields, 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/DeleteRowsWalkerUnsupportedInputFieldShape,MutationInputResolver’s R128-attributed `NestingFieldarm) are gone. Both walkers flatten aNestingFieldinto 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 aCallSiteExtraction.NestedInputFieldaccess 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@conditionare rejected naming R186; nested@nodeIdFK-targets (R189) compose; nested@tableinputs that introduce a second DML target remain R122’s territory. NewDML_INSERT_NESTING_OKflips the formerDML_NESTING_FIELD_DEFERRED. Single-segment access paths emit byte-identically to pre-R186. -
57cb7b0+f1ee7a6; R266 (deleterows-walker-carrier): DELETE mutations onto theDeleteRowswalker carrier (sealedIdentified | Broadcast), mirroring R246/R258’s UPDATE work for the DELETE verb. Row identification is catalog-derived PK-or-UK coverage via the sharedMatchedKeys.firstCoveredmatcher both walkers call (the seam a futureLookupRowscarrier grows from);Identified’s matched key is a single-row guard, `multiRow: trueopts into theBroadcastarm. NewDeleteRowsFieldworn by the migratedMutationDeleteTableField(dropstableInputArg) plus the newMutationDeletePayloadField/MutationBulkDeletePayloadField;MutationDmlRecordFieldnarrowed to{INSERT, UPSERT}andMutationBulkDmlRecordFieldto{INSERT}(compact-ctors reject DELETE). NewDeleteRowsErrorsub-seal (NoUniqueKeyCoverage,UnsupportedInputFieldShape,OverrideConditionNotSupported) undergraphitron.delete-rows.*. Carving DELETE offMutationInputResolver.resolveInputretired the@valuedirective entirely (absorbing R188): the declaration,DIR_VALUE,DmlKind.acceptsValueMarker/requiresPkCoverage, thevalueMarkedNamespartition machinery, andvalue.adocare all deleted;mutation.adocrewritten to the catalog-derived rule. Rework (f1ee7a6) closed the In Review feedback: aligned theBuildContext@lookupKeyrejection message with theFieldBuildertwin, and shipped the execution-tier UK-covering single-row delete over a dedicated public-schemastorage_binfixture (deleteStorageBinByCode, WHERE on the UNIQUEcode/ RETURNING thebin_idPK, 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 onclaude/review-docs-plan-adYJW; step 5 was already retired by the variant-coverage meta-test (GeneratorCoverageTest.everyGraphitronFieldLeafHasAKnownDispatchStatus+VariantCoverageTest.everySealedLeafHasAClassificationCase). The remaining steps 3-4, positioningcode-generation-triggers.adocas a map into theGraphitronSchemaBuilderTesttruth table, were deferred until the sealed hierarchy stabilised; that stabilisation is R279’s (field-first-classification-driver) own deliverable; the doc-as-index work (thecode-generation-triggers.adocabsorption) 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 fromcatalog.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-constantTABLE__CONSTRAINTnamespace got suggestions back in the bare SQL-constraint namespace. The primary surface,BuildContext.parsePathElementvia the newfkCandidateNames(sourceSqlTable, attempt)helper (BuildContext.java:897), shipped both fixes: it scopes candidates toJooqCatalog.foreignKeysTouchingTable(...)(new helper,JooqCatalog.java:270, with a global fallback when the source table has no touching FKs) and switches namespace onattempt.contains("__"), renderingfkJavaConstantNamevs SQL names. This close adds the cheap half of the sibling surface,BuildContext.unknownForeignKeyRejection(:1009, the@reference(key:)/@nodeIdsynthesis miss path): it now mirrors the same__-namespace detection, drawing fromallForeignKeyConstantNames()vsallForeignKeySqlNames(), 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 byJooqCatalogMultiSchemaTest.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. Fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen 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-reviewedfkCandidateNamespattern with the new test and the full build as the safety net.) -
R254 (
048c9c7, bundled): generatedGraphitronSchema.javaand every per-type*Type.javaare now emitted as flat statement-per-element bodies (oneschemaBuilder/blocal 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 overflowedjavac’s expression-attribution recursion during incremental compilation under `quarkus:devon large schemas; flattening removes the unbounded depth. Non-trivial sub-values (synthesised scalars, directive definitions, applied directives, field/argument definitions) are factored intoprivate staticfactory methods viaHelperMethodSink, so emission sites are bare-name references.GraphitronSchemaClassGenerator.generate(:204-235) andObjectTypeGenerator(object:135-148, interface:175-185, union:203-212, field-def sub-chain:240-269) carry the cutover; theAppliedDirectiveEmitter/ directive-definition siblings were already statement-form. Pinned by@PipelineTierSchemaEmissionChainDepthPipelineTest: amaxChainDepthscanner asserts no emitted expression-statement exceeds depth 16 across a federation fixture and a deliberately oversizedLARGE_SDL, with a scanner self-test (maxChainDepth_detectsLongChainbuilds 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_scalesWithSchemaSizepins 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): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25. (Provenance: the implementation reached trunk bundled inside048c9c7, whose message readsR244 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@fieldColumnFieldon the same column (forced by federation@key+@overrideentity dispatch always selectingid), both classifier arms appended the same jOOQTableField, projecting the column twice and spamming jOOQ "Ambiguous match" INFO logs on every fetched row.TypeClassGenerator’s `$fields()accumulator is now aLinkedHashSet<Field<?>>(:215) that dedupes by jOOQFieldidentity while preserving projection order, returned as aListvia anArrayListwrap (:236) so the emitted surface is unchanged; the formerif (!fields.contains(...))guard collapses to a plainaddunder Set semantics (:227-234). Aliased.as(name)projections stay distinct (jOOQ caches oneTableFieldper aliasedTable), so only true duplicates fold. Pinned non-vacuously at the execution tier byFederationEntitiesDispatchTest(:435-464): a federated_entitiesquery selecting bothidandcustomerId(both →customer_id) captures the emitted SQL and assertscustomer_idappears in the projection exactly once, which fails under the oldArrayListaccumulator;@PipelineTierDedupeReferenceProjectionPipelineTestpins the classifier precondition (compositeCompositeColumnField+ siblingColumnFieldover the same column). In Review → Done gate (reviewer session != implementer): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25. (Provenance: granular R255 stage commits were squashed in a rebase; the code reached trunk bundled inside048c9c7.) -
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 aprivate static decode<Type>Key/Keys/Row/Rowshelper on the<Root>Conditionsclass viaCompositeDecodeHelperRegistry, so the call site collapses tohelper(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 aSupplier-lambda-throw trick (CompositeDecodeHelperRegistry.buildHelper,:86-138). The sibling map/list traversal walkers were converted tomap1/list2/elem3bindings, and a registry-less decode now throwsIllegalStateExceptionrather than falling back to the old inline form (:378). Pinned byCompositeDecodeHelperRegistryTest(naming matrix, return types, projection and skip/throw bodies, including arity-1 coverage) and end-to-end by execution-tierGraphQLQueryTest.films_filteredByArgNodeId_dropsWrongTypeIdViaSkipHelper, which confirms a wrong-type id decodes to null and is dropped by the lifted helper’sfilter(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 session0182HAPCJwMRxopaPWwquKyk): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25. -
R64 (retired as obsolete + residual dead-code deletion): the planned lift, making
SplitRowsMethodEmitter.buildRuntimeStubaccept a typedRejection.Deferred/EmitBlockReasoninstead of a free-formString reason, is moot. The premise was thatSplitRowsMethodEmitter.unsupportedReasonreturnedOptional<Rejection.Deferred>and fourbuildFor*callers fed.message()intobuildRuntimeStub; the rows-method rework that followed (thebuildSingle/buildList/buildConnectioncutover) deletedunsupportedReasonand the four.message()call sites entirely, leavingbuildRuntimeStubas a private, uncalled method (verified: zero call sites repo-wide, nounsupportedReason/Rejection.Deferred/EmitBlockReasonreference anywhere in the file). There is nothing left to type-lift. This entry deletes that dead method (and its now-empty// Stubssection header), the actionable residue the obsolete plan left behind. Fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen 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):mdBodyToAdoctranslates markdown tables embedded in.mdroadmap 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 (aMD_TABLE_ROWheader line immediately followed by aMD_TABLE_SEPseparator, then body rows until the first non-table line) and emits[cols="N*", options="header"]+|===, one cell per line.parseMdTableCellsstrips 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 theparseMdTableCellsstrip/unescape edge cases. Scope cut from the spec: the[cols=...]attribute is synthesized from the column count only (equal-widthN*); GFM alignment markers (:---:) are parsed by the separator regex but not carried into per-column alignment, since no roadmap.mdtable uses them and equal-width is the safe container-filling default. Provenance note: the implementation andMdTableToAdocTestwere committed bundled inside048c9c7(whose message readsR244 Ready -> In Progress) and the item never transitioned out ofBacklog; this entry records the retroactive In Review → Done close. Gate (reviewer session != implementer session01YQEc4FG3cqf18pHwgVAnsX): implementation and test verified present in-tree and green on trunk. -
R166 (Backlog, never specced; retired without a landing commit):
graphqlschemavisitor-driven-emissionsuperseded by R279 (field-first-classification-driver). R166 proposed aGraphQLSchemaVisitor-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 prunedGraphitronSchema), so the visitor-driven emitter and R166’s standaloneReachabilityPruneralternative 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 forFetcherRegistrationsEmitter.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 correctionc69bce5): Record binding is reflection-only and sound. The four classifier/binding sites that still read@recordto drive backing-class or kind are gone:RecordBindingResolver.groundServiceField’s `sdlHasRecordgate,TypeBuilder.classifyType’s `|| hasAppliedDirective(DIR_RECORD)arm, and the directive-classNamefallbacks inbuildResultType/buildInputType. The service producer’s reflected return element now grounds the result observation through a sharedgroundProducerResulthelper under a cardinality-match guard (single→single, list→list) plus the@table-backed-SDL andshouldBindguards, so a source-record-carrier payload (no@record) binds to its producer’sJooqTableRecordTyperather than degrading to an unbound plain object; the R75 list-carrier path is preserved.@recordstays a parseable, registered directive with the ignored-directive warning (emitDirectiveIgnoredWarnings/readRecordClassName), so existing schemas keep loading. D1:@recorddropped fromdetectTypeDirectiveConflict(only@tablevs@errorremain mutually exclusive;@table/@error+@recordwarn instead of reject). The reopened completion scope made binding complete and sound:groundComputedFieldgrounds@externalField/ChildField.ComputedFieldthrough the same shared helper, andpropagateAccessorChainsfolds-then-cascades parent-accessor bindings (the root fix for theFilmCardWrapper.film/RecordExample.fieldCexecution regressions).GraphitronType.PlainObjectTypeis eliminated as a terminal classification (Javadoc mention only): a genuinely unbound reachable object is routed toUnclassifiedType/ left absent and surfaces at the field edge asUnclassifiedField(build-time failure, no silent runtime null), with the double-classification guard hardened.GraphitronType.PojoResultType.NoBackingandTypeClassification.UnbackedPojoResultare deleted (PojoResultTypecollapses toBacked), with every consumer updated. The spec-correction commit reverses a conflation: the LSP backing-shape projectionTypeBackingShape.NoBacking.UnbackedResultis kept (it is load-bearing for eleven still-liveGraphitronTypevariants inCatalogBuilder.projectTypeplus three LSP tests); only its stale Javadoc was refreshed. R157’s two LSP backing-shape fixtures migrated from@record(className:)to@serviceproducers. Carve-outs: R277 (@tableMethodunder a table-boundNestingField) filed to Backlog with its execution test@Disabled; R275 consumes R276’s carrier binding; thePlainObjectType/NoBackingdeletions are the subtractive slice R222’s hierarchy cleanup builds on (boundary confirmed non-colliding, R222 still in Spec). Coverage: pipelineR96RecordBindingPipelineTest.unreachable_recordTypeIsIgnored_leftUnclassified+serviceListCarrier_bindsWrapperToJooqTableRecord,SingleRecordPayloadPipelineTestorphan-carrier case,R157PipelineTestLSP migration, the migratedGraphitronSchemaBuilderTestproducer 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): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25 across all eleven modules. -
R214 (
048c9c7, bundled; retroactive close): inferargMappingwhen a@condition/@service/@tableMethodJava signature is unambiguous, so authors no longer have to rename a Java parameter or write a redundantargMapping: "javaName: gqlName"for a structurally-unique pairing.ServiceCatalog.inferBindingsByTypeaugments the name-basedargByJavaNamemap 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 theunambiguousReachablePathdot-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>andSet<>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@conditioninBuildContext); the path-step@conditioninresolveConditionRefhas no slot types and is unaffected. Coverage:TestConditionStub.argConditionTypeUnique/argConditionTwoStringsfixtures plusServiceCatalogTest.reflectTableMethod_typeUniqueSignature_infersBindingWithoutArgMapping(pins the inferredParamSource.Argbinding,whatever→ argopptaksNavn) andreflectTableMethod_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 aJavaTypeKey-counted rule (R219). Provenance note: the implementation, fixtures, and resolver pass-throughs were committed bundled inside048c9c7(whose message readsR244 Ready -> In Progress) and the item never transitioned out ofBacklog; this entry records the retroactive In Review → Done close. Gate (reviewer session != implementer session01YQEc4FG3cqf18pHwgVAnsX): implementation verified present in-tree, and green on trunk through every Done item that landed on top (R244, R246, R195, R271). -
R271 (
07d6c23+ self-reviewabee3f1+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/__vplus the__arg_/__insertKey/__bulkKey/__lookupKey/__setKey/__bulkSetKeybase 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, orfromMapfactories 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_COLUMNonMultiTablePolymorphicEmitterandSplitRowsMethodEmitter) and documented with the DB-column-collision rationale at each constant;GraphitronSchemaClassGenerator’s synthetic-column `__typenameroutes through a namedTYPENAME_COLUMNwhile the federation_entitiesrepresentation-map__typenamestays a literal (the GraphQL introspection meta-field, a distinct concept). The staleChannelCatchArmEmitterjavadoc naming__t/__m(the code emitsmapping/cause) is fixed. The standing rule lands inrewrite-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):@PipelineTierDunderFreeEmissionPipelineTest(in-processTypeSpecscan, non-vacuous by anisNotEmptyassertion) and@CompilationTierGeneratedSourcesLintTest.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): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25; both meta-tests run (not skipped) and pass. -
R195 (
7f7cac0v1 + rescope876a621+ composite/listef8fe80+c3e10a3+ type-mismatch gate18f79a2+ rework36712f7→98b5a93): decode@nodeId(typeName:)into jOOQ-*Record-typed@serviceinput-bean member fields instead of miscompiling to a wire-String→*Recordcast (the R150/R195ClassCastExceptionfamily).InputBeanResolver.buildInputBeanBodynow branches beforeelse -> new Direct()when a member’s element Java type is assignable toorg.jooq.Record: with a resolvable@nodeId(typeName:)it classifies to a newCallSiteExtraction.NodeIdDecodeRecordleaf carrying(encoderClass, typeId, keyColumns, TableRef table, nonNull); otherwise it is a typedResult.Failed, so a jOOQ-record member never falls through toDirectagain.InputBeanInstantiationEmitter(the one reusable emitter) emits per-type concrete helpers (decode<Type>Record(Object) -> <Type>Record, deduped by record type, plus adecode<Type>RecordListstream variant for list members) whose body calls the now-publicNodeIdEncoder.decodeValues(typeId, nodeId)then loads values positionally withdecoded.fromArray(values, Tables.<T>.<col>…); one call regardless of key arity, coercing through the column converter, with no throwawayRecordN, nofromMap(intoMap())round-trip, and no deprecated-for-removalDataType.convert(Object)(so no@SuppressWarningsleaks into the consumer’s*Fetchers; the encoder’s owndecode<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, missingtypeName:, unknown NodeType) plus a member-type-vs-@nodeId-@tablemismatch gate (18f79a2) that fails generation rather than emittingTables.<NodeTable>.<col>references for the wrong record.ServiceMethodCallWalkercarries the leaf through the R238ValueShapere-projection unchanged;ArgCallEmittergets an explicit unreachable arm. Coverage: pipelineNodeIdRecordInputBeanPipelineTest(9 tests, structural only ; helper presence by name, signatures, and adecodeRecordLeafwalk down the classified model assertingNodeIdDecodeRecordtypeId/key-column arity/record class/nonNull, plus malformed-directive and type-mismatchRejection.message()cases; nocode().toString()body matches, perrewrite-design-principles.adocline 131); compilation tiergraphitron-sakila-example(single/composite/list/list-of-composite fixtures compile theirfromArrayfield references against the real jOOQ catalog); execution tierGraphQLQueryTest(round-trips each shape against PostgreSQL, plusassignFilmRecord_wrongTypeNodeId_throwsDecodeMismatchpinning the throw-on-mismatch contract behaviourally). Original R195 framing (top-level@serviceparameter that is a jOOQ record,@field(name:)/@table-on-input translation) deferred (tangled with R97). In Review → Done gate (reviewer session ≠implementer/prior-reviewer): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25. -
R246 (
8a04c0b+975f593+c63c14c+ reworkf3a39ea): UpdateRows walker carrier (R222 UPDATE slice) with PK-or-UK identification.@mutation(typeName: UPDATE)returning its@tabletype directly (or ID) now classifies throughFieldBuilder.classifyUpdateTableField→UpdateRowsWalkerinstead ofMutationInputResolver.MutationUpdateTableFielddrops itstableInputArgcomponent (andtableInputArg()comes off theDmlTableFieldsealed parent; INSERT/DELETE/UPSERT keep their own) and gains two non-Optional slots via the newUpdateRowsFieldinterface: a slimInputArgRef(SDL arg name, input type name, jOOQTableRef, list flag) built directly by FieldBuilder, and anUpdateRowscarrier (sealed, oneIdentifiedarm whose compact constructor enforces non-emptysetColumns) holding theMatchedKey(PrimaryKey/UniqueKey) plus theSetColumn/KeyColumnpartition. The load-bearing claim is PK-or-UK identification: the walker reads jOOQTable.getPrimaryKey()/getKeys()viaJooqCatalog.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: trueon 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 separateUpdateRowsEmitterclass):buildMutationUpdateFetcher/buildBulkUpdateFetcherprojectsetColumns()/keyColumns()back into theSetGroup/InputColumnBindingGroupshapes viasetGroupsOf/keyGroupsOf, emitting byte-identical SQL. Error taxonomy ships as a sibling sub-sealUpdateRowsError implements Rejection.AuthorError(five arms:NoUniqueKeyCoverage,NoSetFields,MixedCarrierKeyMembership,UnsupportedInputFieldShape,OverrideConditionNotSupported), each withlspCode()undergraphitron.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-classifiedInputFieldpermits rather than raw-SDL re-derivation (the R238-style substrate concession, follow-up filed as R257). Coverage: unitUpdateRowsWalkerTest(11 cases); pipelineGraphitronSchemaBuilderTest(typed-arm migrations +R246_UPDATE_MULTIROW_TRUE_DEFERRED+R246_UPDATE_ARG_CONDITION_STRUCTURAL_REJECTED+ R215 inversion); execution via the existingupdateFilmround-trip (UK-driven execution case deferred, justified in as-built notes). In Review → Done gate (reviewer session ≠implementer/prior-reviewer): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25 (graphitron, graphitron-lsp incl.RejectionSeverityCoverageTest, and the execution tier all pass). -
R250 (
73e7670+39072a3):GraphitronSchemaClassGenerator.generatenow emits.withSchemaAppliedDirectives(java.util.List.of(...))on the runtimeschemaBuilder, immediately after theadditionalDirective(survivors)loop and before.codeRegistry(...), so the consumer’sextend 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-internalscompleteSubgraphSchema) failing to detect Fed2 because theschema @link(...)block was missing from the runtime SDL, falling through tocompleteFed1SubgraphSchemaand rejecting the canonically Fed2-shaped@keydeclarations with "argument fields should have type_FieldSet!but foundfederation__FieldSet!`". `AppliedDirectiveEmitter.applicationsForSchema(GraphQLSchema)is the new entry point; the schema-applied list is not aGraphQLDirectiveContainerin graphql-java so the helper takes the rawGraphQLSchemarather than reusingapplicationsFor. Shape contract diverges from the per-container sibling on purpose and is documented inline:applicationsForreturns blocks pre-wrapped in.withAppliedDirective(...)because the per-type builders take one application at a time;applicationsForSchemareturns bareGraphQLAppliedDirective.newDirective()...build()blocks becauseGraphQLSchema.Builder#withSchemaAppliedDirectivestakes a singleList<GraphQLAppliedDirective>. Survivor filter mirrors the per-container path; generator-only directives are skipped. Argument-value rendering routes through the sameValuesResolver.valueToLiteral+AstPrinter.printAst+Parser.parseValuechain the per-type emitter already uses, so@link’s `import: ["@key", ...]round-trips through an AST list literal without per-shape coding. Thelink__Importscalar andlink__Purposeenum referenced by@link’s argument types are already registered on the runtime schema by R248’s `ScalarTypeResolverSynthesised arm and standard enum registration, soemitInputType’s `GraphQLTypeReference.typeRef("link__Import")resolves at schema-build time. Coverage: unit-tierAppliedDirectiveEmitterTest.applicationsForSchema_emitsBlocksForSchemaLevelSurvivorDirectivesandapplicationsForSchema_skipsGeneratorOnlyDirectivespin the helper output for a schema-applied@link(url:..., import:["@key"]); unit-tierGraphitronSchemaClassGeneratorTest.build_emitsWithSchemaAppliedDirectives_forSchemaLevelLinkpins the emitted call site and its position relative to.codeRegistry(...), withbuild_skipsWithSchemaAppliedDirectives_whenNoSchemaLevelSurvivorsas the negative arm; pipeline-tierFederationBuildSmokeTest.serviceSdlExposesSchemaAppliedFederationLinkbuilds the full sakila federated schema, queries_service { sdl }, and asserts the printedschema { ... }block carries@link(url:"https://specs.apollo.dev/federation/v2.10", import:["@key"]), locking the round-trip consumer SDL →applicationsForSchema→withSchemaAppliedDirectives→Federation.transform→_service.sdlagainst silent removal. The R247 file-emission side gains a sibling assertion inSchemaSdlEmissionTestthat the federated file artefact carriesschema @link(, closing the loop on the file the supergraph composer actually reads. Out of scope, follow-up filed: multi-federation-@linkconsumer schemas (FederationLinkApplieralready rejects more than one federation@linkwith a developer-readable error); re-evaluating whetherFederation.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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R247 (
73e7670+39072a3): NewSchemaSdlEmitterruns at the tail ofGraphQLRewriteGenerator.runPipelineand renders the assembledGraphQLSchematotarget/generated-resources/graphitron/<outputPackage as path>/schema.graphqls;GenerateMojoregisters that directory viaproject.addResource(...)so maven-resources-plugin copies it intotarget/classes, shipping the file at<outputPackage as path>/schema.graphqlsin the consumer’s JAR. The federation arm runsFederation.transform(assembled).setFederation2(true).build()beforeServiceSDLPrinter.generateServiceSDLV2, mirroring the consumer’s runtime build so_Service/_entities/_Entityare present on both sides; the non-federation arm uses graphql-java’sSchemaPrinterwithincludeDirectives(true)/includeScalarTypes(true)/includeIntrospectionTypes(false)/includeSchemaDefinition(true).RewriteContextgains anoutputResourcesDirectoryrecord component;AbstractRewriteMojo.resolveOutputResourcesDirectory(basedir)derives it fromproject.getBuild().getDirectory()with abasedir/targetfallback for hand-builtMavenProjecttest fixtures (no@Parameter, no per-consumer toggle); after the self-review followuprunGeneratorreturns theRewriteContextsoGenerateMojo.executereadsoutputDirectoryandoutputResourcesDirectoryfrom one derivation site. Tests: unit-tierSchemaSdlEmitterTest(federation + non-federation + empty-package arms); pipeline-tierSchemaSdlEmissionTest(federated SDL carries the canonical@key, the synthesisedfederation__FieldSetscalar, the@linkdirective declaration, and the schema-applied@link(...)block; non-federation SDL parses throughSchemaParserand is missing the federation surface; classpath resource lookup non-null under bothoutputPackage`s); `GenerateMojoTest.buildContext_derivesResourcesDirectoryFromBuildTargetlocks 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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R251 (
1c1b425):AppliedDirectiveEmitter.buildApplicationnow skips arguments whosegetArgumentValue().isNotSet()is true rather than feeding theNOT_SETslot intoValuesResolver.valueToLiteral(whichassertShouldNeverHappen`s on it). The reconstructed `GraphQLAppliedDirectivecarries 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-tierAppliedDirectiveEmitterTest.omittedArguments_areSkipped_notRenderedAsNotSetLiteralbuilds@audit(reason: "pii")on a directive declaring bothreasonandticketand assertsticketdoes not appear in the emitted.argument(...)chain. R248 fixed the adjacent directive-definition-side bug; R251 closes the application-side symmetry. -
R248 (
a2b1705):DirectiveDefinitionEmitternow round-trips argument default values (.defaultValueProgrammatic(...)emitted via the sameGraphQLValueEmitter.emitpathObjectTypeGenerator.buildArgumentalready uses for field arguments), sodirective @key(resolvable: Boolean = true)survives the JavaPoet reconstruction with its default intact. The federation-namespace scalar fix is a sub-taxonomy lift onScalarResolution: a new sealedSuccessfulinterface (javaType()accessor) sits between the root andResolved, with a siblingSynthesisedarm carrying(javaType, sdlName, coercingSourceOwner, coercingSourceField)for scalars that have nopublic static final GraphQLScalarTypeconstant on the consumer classpath.ScalarTypeResolver.resolveFederationNamespaceScalarreturnsSynthesised(String.class, "federation__FieldSet", _Any, "type")instead of the oldResolved(String, Scalars, "GraphQLString")placeholder;GraphitronType.ScalarType.resolutionwidens fromResolvedtoSuccessful;TypeBuilder’s federation-namespace branch and Java-type registry lookup narrow to `Successful;TypeBuilder.asRejectionswitches onSuccessful(every successful arm throws ; only rejections reach the dispatcher).AppliedDirectiveEmitter.emitInputTypeemitsGraphQLTypeReference.typeRef(name)for federation-namespace scalars instead ofScalars.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: `Resolvedemits the existing.additionalType(Owner.FIELD),Synthesisedemits an inline.additionalType(GraphQLScalarType.newScalar().name(<sdl>).coercing(<owner>.<field>.getCoercing()).build());_Any.type.getCoercing()is the same lever federation-jvm uses inensureFederationV2DirectiveDefinitionsExistwhen synthesising missing federation scalars at the registry+wiring entry point. The misleading "federation-jvmtransform()replaces the placeholder after the base schema is built" comments atScalarTypeResolver.java:83-95/:310-314,TypeBuilder.java:601-605/:647-651, andAppliedDirectiveEmitter.java:122-134retire ;Federation.transform(GraphQLSchema)only adds_Any/_Entity/_Serviceand wires entity resolution, never rewrites@keyor injects scalars; the divergence reached the printed Service SDL untouched. Coverage: pipeline-tierFederationBuildSmokeTest.serviceSdlExposesCanonicalKeyDirectiveShapeasserts the printed SDL carriesdirective @key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACEandscalar federation__FieldSet(the end-to-end behavior subgraph-composition tooling validates against);DirectiveDefinitionEmitterTestextends the existing argument-emit test with a.defaultValueProgrammatic(+"strict"assertion and adds a Boolean-default arm (@flag(enabled: Boolean = true)) to cover theGraphQLValueEmitter.emitdispatch on a different value shape;ScalarTypeResolverTestflips the federation-namespace resolver test to expectSynthesised(_Any, "type")and adds a second arm onlink__Importto confirm the dispatch isn’t FieldSet-specific;GraphitronSchemaBuilderTestnarrows existingScalarType.resolution()reads to theResolvedarm they’re testing. Out of scope (called out): argument-level@deprecatedon directive definitions (no survivor directive Graphitron emits today carries one); re-emitting@linkitself (the federation library injects@linkinto the registry viaLinkDirectiveProcessor, the survivor walker picks it up like any other directive); switching graphitron’s federation entry point fromFederation.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) sinceFederationSpec.URLpins v2. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R237 (
96869ab+e24feac+ee9720f+246b349+aa83a16+8845717+18a37a2): Retire the@LoadBearingClassifierCheck/@DependsOnClassifierCheckannotation pair and theLoadBearingGuaranteeAuditTestaudit 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-recordparentTablelift toColumnFieldrecord component) and R240 (service-catalog-strict-tablemethod-return+tablemethod-resolver-return-is-table-bound, b-relational, type-token threading onMethodRef.StaticOnly×ReturnTypeRef.TableBoundReturnType). Knock-on:PkResolutionEmitterReachabilityTest.classifyDeleteTableProjectionWearsLoadBearingClassifierCheckPinretired 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 retiredload-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.adocPhase 1 rewrite shipped at96869abanchored the principle on the three surviving layers (type-system narrowing at producer, pipeline-tier tests, cross-module compile againstgraphitron-sakila-example); theprinciples-architectagent andsrp/reviewer-promptskill rubrics swapped the "Load-bearing classifier checks" rubric for "Missing type-system lift". Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R233 (
01c0172+d02859f): LSP@field(name:)completion + hover arms now resolve the column against the@referencepath’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.FieldClassificationgains a sealed nestedLspColumnDispatch(permitsResolve(tableName)/Silent/FallThrough) and an exhaustivelspColumnDispatch()default method that switches over all 30 sealed permits with nodefaultarm ; 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) produceResolve(tableName)carrying R224’s already-projected terminal table;InputUnbound/UnclassifiedproduceSilent; every other permit producesFallThrough. The three consumer sites (Diagnostics.validateFieldMember,FieldCompletions.completionsFor,Hovers.columnHover) collapse to a uniform 3-arm switch on the projection:ResolveandSilentreturn directly,FallThroughdrops through to the existing backing-driven dispatch.FieldCompletionsswitches fromTypeContext.enclosingFieldDefinitiontoenclosingFieldOrInputValueDefinition(R224’s helper) so input-sideinput_value_definitionnodes resolve too;Hovers.columnHoveradopts the same helper. A smallmergeWithSigilhelper inFieldCompletionsshares the$source-sigil merge between the new dispatched arm and the existing backing arm. Annotation hygiene:field-classification-payload-faithfulnow has five consumer sites (InlayHints.compute,DeclarationHovers.compute,Diagnostics.validateFieldMember,FieldCompletions.completionsFor,Hovers.columnHover); the producer description atCatalogBuilderenumerates the five consumers and nameslspColumnDispatch()as the routing primitive. Tests: pipeline-tierLspColumnDispatchProjectionTestdrives the full classifier on a synthetic schema and pins the three arms (Resolve / Silent / FallThrough) plus a cross-permit invariant thatColumnReference.tableName() == Resolve.tableName();FieldCompletionsTestgains three R233 regressions parallel to R224’sDiagnosticsTestcases (inputTableWithReferencePathCompletesTerminalTableColumns,outputTableWithReferencePathCompletesTerminalTableColumns,unresolvedReferencePathCompletionSilentOnLspSide);HoversTestgains three symmetric regressions (inputTableWithReferencePathHoversOnTerminalTableColumn,outputTableWithReferencePathHoversOnTerminalTableColumn,unresolvedReferencePathHoverSilentOnLspSide); R224’s threeDiagnosticsTestregressions stay green untouched. Self-review cleanup (d02859f) inverted the spec’s double-Optional-of-Optionaldispatch shape at the two new consumer sites to direct returns (ResolveandSilenteach return directly,FallThroughdrops through), preserving the exhaustiveness guarantee while removing the nested generic. Out of scope (called out, filed as R236): the runtime-sideBuildContext.classifyInputFieldInternalcandidate hint atBuildContext.java:1673draws 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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon 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-affectedChildFieldvariants (TableField,LookupTableField,SplitTableField,SplitLookupTableField,RecordTableField,RecordLookupTableField) plus the seventh siblingColumnReferenceField.JoinStep.ConditionJoin’s record header gains a `TableRef targetTableresolved at parse time byBuildContext.resolveConditionJoinTarget; terminal hop from the carrier field’s return-type@tablebinding, intermediate hop by reflecting on the condition method’s second parameter type viaJooqCatalog.findTableByClass; with a compact-constructor null-check as the structural safety net behind the newcondition-join.target-table-resolved-at-parseload-bearing-classifier key. TheWithTargetcapability splits into a smallerHasTargetTable(target read only) plus the slot-iterationWithTarget extends HasTargetTable, soConditionJoinjoinsFkJoin/LiftedHopunder one capability for the alias-declaration loop andJoinPathEmitter.targetJavaClassNamecollapse. A new sealedParentCorrelationtaxonomy (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 affectedChildFieldvariant’s record header with aparentCorrelation.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 freshparentAliasTable local for the @table-bound parent, emits the step-0.join(parentAlias).on(condition(...))clause, and routesparentInputto JOIN on parent-PK columns. Validator-side:validateVariantIsImplemented’s `SplitRowsMethodEmitter.unsupportedReasonconsult andvalidateColumnReferenceField’s `hasConditionJoinbranch both delete;validateReferenceLeadsToTypefolds itsWithTargetspecial-case onto a uniformHasTargetTableread. TheConditionJoinReportablecapability interface deletes outright;Rejection.EmitBlockReasonenum (six values, all condition-join-step) andRejection.StubKey.EmitBlockrecord retire with their last producers;JoinPathEmitter.hasConditionJoinpredicate retires with its last consumer;docs/manual/reference/diagnostics-glossary.adoc’s six `=== <variant>-condition-join-stepheadings delete perDiagnosticsDocCoverageTest. R129 absorption: thecolumn-reference-on-scalar-field-condition-joinslug closes ;ColumnReferenceFieldwith a multi-hop path containing a condition step now classifies and emits viaInlineColumnReferenceFieldEmitter; 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*ValidationTestcases flip from deferred-rejection to no-error; newHasTargetTableInvariantTestpins the JoinStep-permits-implement-HasTargetTable invariant;ParentCorrelationFirstHopInvariantTestexercises bothOnFkSlotsandOnConditionJoinarms end-to-end;GraphitronSchemaBuilderTestgainsCONDITION_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-hoptargetTable), andCONDITION_INTERMEDIATE_TABLE_WILDCARD_REJECTED(AUTHOR_ERROR when the intermediate condition method usesTable<?>); plus extends the existingWITH_CONDITION_PATHfixture with a non-nulltargetTable()check. Symmetric finish onRecordTableMethodField: the seventh@record-parent variant gains aParentCorrelationfield on its record header so a {condition:}-first path AUTHOR_ERRORs at parse time (same shape as the siblingRecordTableField/RecordLookupTableFieldvariants ;@record-parents have no@tableto anchor the condition method’s source arg, so the synthesis routes through AuthorError); the deadinstanceof JoinStep.FkJoinarm inSplitRowsMethodEmitter.buildForRecordTableMethod’s `unsupportedPathpredicate 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 andFilm.actorsByCondition: [Actor!]! @splitQuery @reference(path: [{condition: ...}])exercising the split-rows emission shape via an EXISTS-over-junction predicate;ReferencePathConditionFixturesships the two condition methods. Compile-tier coverage flows throughmvn install(generated code compiles against the real jOOQ catalog); execution-tierGraphQLQueryTest.inlineTableField_conditionJoin_returnsAddressPerCustomerandsplitTableField_conditionJoin_returnsActorsPerFilmassert end-to-end SQL correctness against PostgreSQL (the inline test cross-checks against the FK-equivalentCustomer.addressnavigation; the split-rows test verifies one batched DataLoader round-trip across five films). Out of scope, follow-up filed: the legacyReferenceElement { 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):EnumTypeGeneratornow 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 newno.sikt.graphitron.rewrite.model.EnumValueSpec(sdlName, runtimeValue, description, deprecationReason, source) carried asList<EnumValueSpec> valuesonGraphitronType.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"forFODSELSNUMMER @field(name: ...)) hitCan'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-sideCallSiteExtraction.TextMapLookuparm intoDirect: the sealed permit,EnumMappingResolver.enrichArgExtractions, theTypeConditionsGenerator/TypeFetcherGeneratorstatic*_MAPemit paths, theArgCallEmitter/FieldBuilderswitch arms, and theEnumMappingResolverfield/constructor params onServiceDirectiveResolver/TableMethodDirectiveResolverall retire.EnumMappingResolver.buildTextEnumMapping/.validateEnumFilterread from the classified model’sList<EnumValueSpec>(lookup viactx.types) so the directive is read once at classify time and both consumers share the record component, eliminating the drift R263 reintroduced. Coverage: pipeline-tierGraphitronSchemaBuilderTest.EnumTypeCase.ENUM_WITH_FIELD_NAME_DIRECTIVEpins the classifier output (runtimeValue == "FØDSELSNUMMER"); existing PLAIN_ENUM / ENUM_WITH_DEPRECATED_VALUE cases reroute throughEnumValueSpec; new execution-tierEnumSerializationExecutionTestpins 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-tierEnumTypeGeneratorTest.typeMethod_routesFieldNameDirectiveIntoRuntimeValuepins the generated.name(sdl).value(runtime)shape; the R53 regressionSERVICE_MUTATION_FIELD_NAME_OVERRIDE_TEXT_ENUMflips its assertion fromTextMapLookuptoDirect(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 typeStringat emit time, so R229’s.value()lift is invisible to clients on Sakila’stextRatingfield; emitting those fields as the enum type is a separate structural fix. Self-review follow-upb9121e0swept stale TextMapLookup /enrichArgExtractionsjavadoc citations acrossInputBeanResolver,ServiceCatalog,ServiceDirectiveResolver,TableMethodDirectiveResolver,ArgCallEmitter,BodyParam,ConditionFilter,InputColumnBinding,InputField, andMethodRef. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R230 (
8f59529): FixBodyParam.nonNullfor nested input fields under a nullable enclosing arg.walkInputFieldConditionsnow ANDs aneffectiveNonNullboolean (seeded from theInputTypeArg’s `nonNull()atprojectFilters, narrowed at eachNestingFieldrecursion) into the value passed to everyimplicitBodyParam/compositeImplicitBodyParamcallsite, so the emitter’s unguardedcondition.and(...)branch only fires when every enclosing link is statically non-null. Pre-R230 a query likesoknader(filter: HentSoknadInput): [Soknad!]withHentSoknadInput.soknadId: [ID!]!silently returned the empty set whenfilterwas omitted, because the generator emittedcondition.and(film.film_id.in(null))and jOOQ renders.in(null)as the literalfalse. Producer contract pinned by a newbody-param.nonnull-is-effective-runtime@LoadBearingClassifierCheckonwalkInputFieldConditions(single annotation covers both producer sites in its description text ; audit requires producer-key uniqueness) paired with a@DependsOnClassifierCheckonTypeConditionsGenerator.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: `NestedInputFieldEffectiveNonNullPipelineTestpins the three AND transitions on the classified slot (nullable arg → false, both non-null → true, nullableNestingFieldwrapper between non-null arg and non-null leaf → false);GraphQLQueryTest.filmsByEffectiveNullability_omittedFilter_returnsUnfilteredBaselineis 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-toolgains acheck-adoc-tablesverify-phase subcommand that walks every authored.adocundergraphitron-rewrite/anddocs/, 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 atgraphitron-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;.mdfiles are out of scope (markdown table syntax is native there).AdocMarkdownTableCheckTestpins 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.mdfiles 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):mdBodyToAdocdoes not translate markdown tables embedded in.mdroadmap plans, so rendered roadmap.adocundertarget/still carries raw pipe rows; that render-side hole is tracked separately. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; verify phase reports "no markdown-formatted tables in authored .adoc files". -
R228 (
de25b0a+ac5830d):GraphitronSchemaValidatornow rejects inlineTableField/LookupTableFieldwhose@referencepath carries a@conditionstep at build time, closing the gap that let those two variants reach a runtimeUnsupportedOperationExceptionwhile the four sibling variants surfaced aRejection.Deferredbuild error.ChildField.TableFieldandChildField.LookupTableFielddeclareConditionJoinReportablewith their ownEmitBlockReasonvalues (TABLE_FIELD_CONDITION_JOIN_STEP,LOOKUP_TABLE_FIELD_CONDITION_JOIN_STEP) anddisplayLabel("Inline TableField", "Inline LookupTableField");SplitRowsMethodEmitter.unsupportedReasonis the single predicate the validator and both inline emitters consult, so inline stubs render byte-for-byte the same message as before and the fourfour ChildField variantsjavadoc/comment sites widen to six. Tests:R58TypedRejectionPipelineTestgainsinlineTableField_conditionJoinStep_rejectedAtBuildTime+inlineLookupTableField_conditionJoinStep_rejectedAtBuildTimepipeline-tier coverage; the existing seal-tracking assertion renamesconditionJoinReportable_implementedByExpectedFourVariants→conditionJoinReportable_implementedByExpectedSixVariants;TableFieldValidationTest/LookupTableFieldValidationTestflipWITH_CONDITION_ONLYfrom "no error" to "stub surfaces as build error" and addLIST_WITH_CONDITION_ONLY.docs/manual/reference/diagnostics-glossary.adocadds=== table-field-condition-join-stepand=== lookup-table-field-condition-join-stepparagraphs alongside the four existing entries (DiagnosticsDocCoverageTestgate). Sakila example:Category.similarwas an inline-TableFieldConditionJoin 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 dependentCategoryConditionsclass are removed from the example along with theREADME.adocreference. 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 inSplitRowsMethodEmitter. Note on test placement: spec namedGraphitronSchemaBuilderTestbutGraphitronSchemacarries norejections()accessor; fixtures landed inR58TypedRejectionPipelineTest(existing home for "build schema + run validator + assert typed rejection"). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R224 (
5b202fb+d1e8fd6): LSPDiagnostics.validateFieldMemberconsults 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. ForColumnReference/CompositeColumnReferencearms the lookup now resolves throughFieldClassification.tableName()(projected viaCatalogBuilder.terminalTableName), mirroring the runtime’sServiceCatalog.resolveColumnForReferencewalk;Column/CompositeColumnarms route the same way (equivalent target table, sourced from the classification);InputUnbound/Unclassifiedarms stay silent because the validator already emits a precise message; other arms fall through to the existing backing-driven dispatch. The fix also addsTypeContext.enclosingFieldOrInputValueDefinitionso the dispatch resolves the SDL field name on input-sideinput_value_definitionnodes too (the priorenclosingFieldDefinitionwalked only output-sidefield_definition). The new emitter site wears@DependsOnClassifierCheck(key = "field-classification-payload-faithful")against the existingCatalogBuilderproducer. Tests:DiagnosticsTestgains three regression cases driving syntheticLspSchemaSnapshot.Built.Currentsnapshots: input@table+@referenceretargets to terminal-table column, output@table+@referencemirror, and silence-on-Unclassified(no duplicate "Unknown column … on table '<enclosing>'" diagnostic). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R225 (
9b753db): LSPDiagnostics.severityOfflipsRejection.DeferredfromDiagnosticSeverity.WarningtoError, so the editor squiggle matches themvn graphitron:devfinality (everyRejectionvariant throwsValidationFailedException, regardless of arm ; the actionable hint is the roadmap-item slug carried by the rejection, not the severity, reverting the R147 softening).ValidatorDiagnosticsTest.deferredMapsToWarningSeverityrenamed todeferredMapsToErrorSeveritywith its severity assertion flipped;RejectionSeverityCoverageTestunchanged (asserts only non-null). Build green: fullgraphitron-lsptest 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 walkextend type X { ... }declarations in parallel withtype X { ... }definitions. A new closed-familyDeclarationKindenum (graphitron-lsp/.../parsing/DeclarationKind.java, 12 constants spanning both*_type_definitionand*_type_extensionkinds) replaces the three out-of-syncSet<String>sources of truth inInlayHints,TypeContext, andDeclarationHovers;DeclarationKind.enclosing(Node)+DeclarationKind.walkAll(Node, Consumer)centralise the two walks every consumer used;isCarrier()filters the field-hover ancestor walk to coordinates whereParent.fieldNameis meaningful.TypeContext.tableNameOfis rerouted through the classifier’s name-keyed projection on the snapshot (built.typeClassificationsByName().get(name)→tableNameFromClassification) so anextend type Customer { ... }whose@table-bearing definition lives in another file still resolves to the authoritative table name; the privatetableNameOf(TypeClassification)helper inInlayHintslifts toTypeContext.tableNameFromClassificationso inlay / hover / completion / definition / diagnostic surfaces share one switch. Snapshot threading reachesReferenceCompletions.generateandDefinitions.compute(both gain@DependsOnClassifierCheck(key = "type-classification-payload-faithful")so the audit test pins their new dependence on the type-classification projection);GraphitronTextDocumentServicewiresworkspace.snapshot()to both. Tests:InlayHintsTestgains classification, inferred-@field, and absent-@tableparity onextend type Query+extend type Customer;DeclarationHoversTestgains type-name and field-name hover parity insideextend type Customer;DiagnosticsTestgains@field(name:)member validation insideextend type Foo(unknown column + valid column); the existingReferenceCompletionsTest.unknownTableReturnsEmptyForKeyadapts to the snapshot-as-source-of-truth posture (classifier mapsFooto a missing table, completion empties). Self-review fix (70e41cb) dropped a defensiveLinkedHashSet<Node>dedupe fromwalkAll; tree-sitter ASTs are trees, the set guarded against an impossible scenario. Out of scope (called out): generator-side admission ofextend type Foo @table(name:"x") { ... }(the classifier doesn’t see@tableon extensions today, so the snapshot-routedtableNameOfstays silent on extension-declared@tablewithout a corresponding definition ; lifting that constraint is a classifier-side change with its own roadmap item). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon 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 isTable/Node/TableInterface/TableInputbut that carry no@tabledirective at all.LspClassificationLabels.projectionLabel/projectionTypeLabelreturn each projection record’ssimpleName()verbatim ("Column", "Table", "DmlMutation", …) via exhaustive uniform-body switches that survive as compile-time tripwires for new permits;DeclarationHoversprints the qualified form (**FieldClassification.Column**/**TypeClassification.Table**) in hover headers;FieldClassification/TypeClassificationclass-level Javadoc records the new dual role of projection-record names. The generator-permitfieldLabel(GraphitronField)/typeLabel(GraphitronType)variants had no LSP callers and are deleted.InferredDirectiveArgs.Entrygains anAbsentArm absentArmslot (initial implementation used aboolean renderWhenAbsentflag, replaced in the self-review fix by a sealedAbsentArmstrategy 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@tableentry carries an arm (AbsentArm.TableName) whose switch encodes the eligibility set and readstableName()offTypeClassification.{Table, Node, TableInterface, TableInput};@field/@referencestay off per the spec’s judgement calls.InlayHints.collectAbsentDirectiveHintswalks type-definition nodes in parallel with the classification arm, dispatching toentry.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 theconfig.inferredDirectives()toggle. The@DependsOnClassifierCheck(key="type-classification-payload-faithful")reliesOntext widens to note the absent-directive arm. Tests:InlayHintsTest.classificationHintsLabelFieldDeclarationsflips from"table type", "column"to"Table", "Column"; newabsentTableHintRendersOnObjectTypeWithoutDirective,absentTableHintRendersOnInputTypeWithoutDirective, andabsentTableHintSuppressedWhenDirectivePresentpin the new arm on object + input declarations and assert it stays quiet when the directive node is present;inferredTableHintSuppressedWhenAuthoredextends tononeMatch(label -> label.startsWith("@table"));DeclarationHoversTestflips header assertions to the qualified form. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; 337 graphitron-lsp tests + 1897 catalog tests pass. -
R215 (
fdb757b+3d60f40): column-binding requirement captured at classification, not derived at usage.BuildContext.classifyInputFieldgains aClassifyContextparameter (carriesexpandingTypes+enclosingOverride); the recursive descent throughNestingFieldcomposesctx.expanding(typeName).withOverride(ctx.enclosingOverride() || nestOverride).InputField.ConditionOnlyFieldrenamed toInputField.UnboundField(parentTypeName, name, location, typeName, nonNull, list, Optional<ArgConditionRef> condition, String attemptedColumnName); the classifier emits this variant uniformly on column-miss (across plain and@tableinputs) and on@condition(override: true)with a matching column (the §5ColumnField+override:truecollapse).TypeBuilder.buildTableInputTypedefers column-coverage to consumption (admitsUnboundFieldinstead of rejecting the whole type asUnclassifiedType);FieldBuilder.walkInputFieldConditionsbecomes a single exhaustive switch with theUnboundFieldarm consumingenclosingOverridedirectly and emitting a consumer-side rejection (typedRejection.AuthorError.UnknownNamewith Levenshtein hint) when the cascade doesn’t admit.GraphitronSchemaValidatorwalksTableInputType.inputFields()and rejectsUnboundField + @condition(override: false)at the directive’s source location;MutationInputResolver.resolveInputadmitsUnboundField(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 LSPLspClassificationLabels+DeclarationHoversandFieldClassification.InputUnboundrenamed fromInputCondition);InputFieldResolver.resolve(typeName, rt, enclosingOverride)takes the cascade flag fromFieldBuilder.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) andinput-field.unbound-with-override-condition-admits-on-mutation-update-delete(producer:resolveInput). Coverage: eight new R215 acceptance tests inGraphitronSchemaBuilderTest(r215_plainInputArgLevelOverrideAdmitsNonBindingField,r215_tableInputNonBindingFieldRejectsAtConsumer,r215_tableInputNonBindingFieldAdmittedUnderOverrideCascade,r215_validatorRejectsOverrideFalseOnNonBindingField,r215_validatorRejectsConditionOverrideFalseOnMutationInputField,r215_mutationUpdateConditionOverrideTrueOnNonPkFieldAdmits,r215_mutationInsertConditionOverrideTrueRejects,r215_nestedPlainInputPropagatesCascade); R210’s renamedplainInput_overrideTrueWithoutMatchingColumn_classifiesAsUnboundField+tableInput_overrideTrueWithoutMatchingColumn_classifiesAsUnboundFieldstay 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 withprinciples-architect, alf’s pushback on cascade-contract gloss): the first-passwalkInputFieldConditionsUnboundField arm silently dropped the inner@conditionunder an outer@condition(override:true)cascade, contradictingdocs/manual/how-to/migrating-from-legacy.adoc#behavior-divergence-condition-cascade("every@conditionyou write produces SQL; the override flag controls only the implicit column predicate"). The arm now mirrors theColumnFieldarm structure (always emit the explicit@conditionwhen present; decide rejection separately): rejects at the consumer outside the cascade forcondition.isEmpty()(no filter contribution) andcondition.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 #11r215_innerExplicitConditionFiresOnUnboundFieldUnderOverrideCascadepins the cascade-doc contract (twoConditionFilter`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 fromMutationInputResolverfor 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 aMutationFieldprojection yet); R213 exact-SourceLocationattribution on the surroundingUnclassifiedField’s `locationfield (the rejection prose names the field but the wrapper’s location still points at the consuming query field; threading the location throughwalkInputFieldConditions→projectFilters→projectForFilter→TableFieldComponents.Rejectedis the R213 follow-up); R221 validator walksPlainInputArg.fields()for the sameUnboundField + @condition(override:false)rejection the shipped validator catches onTableInputType(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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon 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.classifyInputFieldInternalreinstates anerrorsBeforesize-delta check inside the R210 override:true block: whenbuildInputFieldConditionappends toerrorsand returns empty, the gate returns a placeholderInputFieldResolution.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 typedAuthorError.UnknownNamelift survives (InputFieldResolver.resolve’s `canLiftToUnknownNameguard seescondErrorsnon-empty +lookupColumnnull and folds toRejection.structural, which is the right bucket ; the failure shape is condition-method binding, not unknown-column). R210’s existing testplainInput_overrideTrueWithBrokenCondition_rejectsAsUnclassifiedFieldgains adoesNotContain("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 productionopptak-subgraph(parameter-name mismatches inOpptakFilterInput.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); thedoesNotContainassertion stays as a regression guard. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R203 (
2b8b465+8d39ef4+9ac3e4cPhase 1 natives module + release workflow; first releaseno.sikt:graphitron-tree-sitter-natives:0.26.0-1published to Maven Central via thetree-sitter-natives-releaseworkflow on its four-platform matrix [Phase 2];ae486d9Phase 3 graphitron-lsp cutover;bbabb02BundledLibraryLookupprobe for well-knownlibtree-sitterinstall prefixes;a94f546In Review → Ready rework after first review pass;a3338b6Phase 4 distinguishes too-oldlibtree-sitterfrom missing and ships the spec-named error-translation unit test): graphitron-lsp no longer compiles a per-platform tree-sitter runtime + grammar on everymvn install. The vendoredlibtree-sitterruntime sources and the bkegleytree-sitter-graphqlgrammarparser.care gone fromgraphitron-lsp/src/main/native/(≈30 000 lines deleted); the threebuild-native-*Maven profiles and theexec-maven-pluginshell-out are gone fromgraphitron-lsp/pom.xml. The grammar binary now ships from a new same-repo standalone Maven modulegraphitron-rewrite/graphitron-tree-sitter-natives/(groupIdno.sikt, version stream<tree-sitter-runtime-ABI>-<build-n>, first release0.26.0-1; standalone pom intentionally not a child ofgraphitron-rewrite-parentand not in the parent reactor’s<modules>list, so the release cadence decouples from the rewrite’s10-SNAPSHOTparent andmvn install -f graphitron-rewrite/pom.xml -Plocal-dbpays zero build cost). The natives release workflow isworkflow_dispatch-only on a four-platform GitHub Actions matrix (linux-x86_64,linux-aarch64,macos-aarch64,windows-x86_64;macos-x86_64dropped during Phase 1 dry-run since Sikt LSP developers all run M1+); each matrix runner runs upstream’stree-sitter buildCLI against the vendored grammar to produce one platform-shaped shared library, the jar carries exactly fourlib/<os>-<arch>/tree-sitter-graphql.{so,dylib,dll}entries (POSIXlibprefix; 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-installedlibtree-sitteron every platform. graphitron-lspBundledLibraryLookupswitches to the four-platform set, drops the previousUnsupportedOperationExceptionWindows branch, and now also probes well-knownlibtree-sitterinstall prefixes (Homebrew/opt/homebrew/lib+/usr/local/libon macOS, vcpkg’s<VCPKG_ROOT|VCPKG_INSTALLATION_ROOT>/installed/x64-windows/bin+ the defaultC:\vcpkg\...on Windows,/usr/local/libon Linux), composing a system-installed runtime onto the SPI grammar lookup viaSymbolLookup.orso vanillabrew install tree-sitter/vcpkg install tree-sitter:x64-windowswork with no env-var wiring.GraphqlLanguage.loadOrExplaintranslatesUnsatisfiedLinkError/RuntimeExceptionfromLanguage.loadinto an install-instructions message and now also distinguishes "too-oldlibtree-sitterinstalled" (commonly Debian/Ubuntu apt’slibtree-sitter00.20.x, which predates thets_language_abi_versionsymbol jtreesitter 0.26 looks up) via a probe-path classifier that walks aBundledLibraryLookup-superset including apt’s/usr/lib/<arch>-linux-gnu/libtree-sitter.so.0and checks the ABI symbol directly.GraphqlLanguageErrorTranslationTestpins 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 nameslibtree-sitter0in the too-old Linux hint.NativeLibraryBundleTestcovers all four platforms via per-platform@EnabledOnOsmethods (3 skipped on any single host);rewrite-build.ymlsource-buildslibtree-sitter v0.26.9so thelinux-x86_64method runs green in CI.getting-started.adocgains 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 theJAVA_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@EnabledOnOsNativeLibraryBundleTestmethods, not a per-PR matrix onrewrite-build.yml. 334 graphitron-lsp tests pass (7 skipped: 3 platform-gatedNativeLibraryBundleTest+ 4 platform-gatedGraphqlLanguageErrorTranslationTestmethods on the Linux CI host); fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on Java 25. -
R210 (
94bc3bf+47f5f39+ac2588c):@condition(override: true)on an input field with no matching column no longer rejects asUnresolvedunder R205’s Path B. NewInputField.ConditionOnlyFieldsealed permit carries theArgConditionRefwith no column data;BuildContext.classifyInputFieldInternalgates on the directive’soverrideflag at the "no column found" fall-through (cheap read, noerrors-list side effects) before building the condition, so the typedAuthorError.UnknownNamelift onoverride:falsereflection failures is preserved at the R205 boundary.FieldBuilder.walkInputFieldConditionsemits the explicitConditionFilteronly; six other exhaustiveInputFieldconsumer sites (ContextArgumentClassifier,GraphitronSchemaValidator,EnumMappingResolver,CatalogBuilder+ newFieldClassification.InputConditionrecord,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.sakskodeshape) and@tableinputs since both shareclassifyInputFieldInternal. Coverage: three R210 acceptance tests (plainInput_overrideTrueWithoutMatchingColumn_classifiesAsConditionOnlyFieldwith@ProjectionFor(ConditionOnlyField.class),tableInput_overrideTrueWithoutMatchingColumn_classifiesAsConditionOnlyField,plainInput_overrideTrueWithBrokenCondition_rejectsAsUnclassifiedField) plus a boundary test (plainInput_overrideFalseWithoutMatchingColumn_stillRejectsAsUnclassifiedField) pinning the R205↔R210 behaviour boundary by name;TestConditionStubgainssakskodeCondition/syntheticNameConditionfixtures;VariantCoverageTest.NO_CASE_REQUIREDcarries 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 exercisesclassifyInputFieldInternaland the projectedConditionFilter). Design alternative considered:InputFieldResolution.ConditionOnlyarm (resolution-tier sibling, structurally honest, deferred to a future refactor if the carrier-vs-resolution distinction becomes load-bearing); the carrier-tierInputField.ConditionOnlyFieldpermit chosen for incremental change cost + uniform walking. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; 1893 tests pass. -
R205 (
fdada52restore plain-input filter symmetry + escalate Unresolved to build error,c1de3f8self-review follow-ups + R209 backlog stub): plain-input (non-@table) filter input types now classify and project identically to@tableinputs.InputFieldResolver.resolvereturns sealedResolution.{Ok, Rejected}(mirroringOrderByResolver.Resolved); anyInputFieldResolution.Unresolvedor@conditionreflection failure lifts as a typedRejection(single column-miss →Rejection.unknownColumnso LSP fix-its consume the structuredattempt + candidates; everything else folds toRejection.structuralwith joined prose).ArgumentRef.UnclassifiedArg’s `String reasonbecomesRejection rejectionwith a backwards-compatiblereason()accessor; the four other construction sites inFieldBuilder.classifyArgumentwrap their prose withRejection.structural(...).FieldBuilder.projectFilters’ `PlainInputArgbranch is now structurally identical toTableInputArg: it allocates a non-nullimplicitBodyParamsand drains it intobodyParams, so the symmetric implicit-predicate emission is a type-system fact (the four per-callimplicitBodyParams != nullguards inwalkInputFieldConditionscollapse; the method asserts non-null at entry viarequireNonNull).projectFilters/projectForFilterthreadList<Rejection>end-to-end via a newfoldRejectionshelper;UnclassifiedArg.rejection.prefixedWith(...)preserves typed payloads (e.g.AuthorError.UnknownNamefrom a plain-input column miss) through toUnclassifiedField.rejection.ProjectionCoverageTestdrops thePojoInputTypeallowlist 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@conditionreflection failure (Path B: bare-field-without-@conditionsignals binding intent just as much as@condition-annotated does). ThelanguagesByPlainInputsakila-example fixture + execution test, which encoded the silent-drop as expected behaviour, are deleted;docs/argument-resolution.adocretires the per-field-skip rationale paragraph at:400-412and 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@conditionmethod when no column resolves;FieldRegistryclassify-input trace’s typed-Rejection payload loss (filed as R209). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R206 (
3a44d47): synthesisedConnectionType/EdgeTypecarry the@asConnectioncarrier field’sSourceLocation.ConnectionPromoter.promotenow passesBuildContext.locationOf(fieldDef)into the two record constructors instead ofnull; first-write-wins on dedupe (the existinginstanceof ConnectionTypeearly-continuepreserves the first carrier’s location) andPageInfoType.location()deliberately staysnullbecause a single PageInfo serves every connection so no carrier site is the actionable one. Downstream,GraphitronSchemaBuilder.rejectCaseInsensitiveTypeCollisionsalready readsexisting.location()when demoting toUnclassifiedType, so SYNTH_VS_SYNTH / SDL_VS_SYNTH / SYNTH_EDGE_VS_SDLValidationError`s now carry an actionable position an LSP/editor can jump to. `CaseInsensitiveTypeClashCasemigratedConsumer<GraphitronSchema>→BiConsumer<GraphitronSchema, String>so each arm sees its own SDL fixture; SYNTH_VS_SYNTH pins both line and column (via newTestSchemaHelper.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’snulllocation locking in the design choice. No record-shape changes; pure provenance threading. -
R204 (
930739a): validate uniformenv.getSource()domain return type acrossOutputFieldproducers on an SDL type. LiftsOutputFieldas a sealed sub-interface ofGraphitronField(permits RootField, ChildField) declaringDomainReturnType domainReturnType(); new sealedDomainReturnType(Record(TableRef)|TableRecord(ClassName)|Plain(ClassName)) mirrors the producer’senv.getSource()Java domain identity without classloading at validator time. Post-classificationGraphitronSchemaBuilder.validateUniformDomainReturnTypegroupsOutputFieldentries by SDL Object return-type name and demotes every participant in a multi-arm group toUnclassifiedFieldwith a typedRejection.AuthorError.MultiProducerDomainTypeDisagreement; the validator carries@LoadBearingClassifierCheck(key = "output-fields.uniform-domain-return-type")paired with a matching@DependsOnClassifierCheckonFetcherEmitter.buildSingleRecordTableFetcherValue. The two formerly-@Disabledmixed-producer cases inSingleRecordTableFieldServiceProducerPipelineTestnow assert against the unified-path diagnostic; unit-tierDomainReturnTypeCoverageTestwalks 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 answerRecord(table)rather thanTableRecord(recordClass)because typedXRecordIS-A jOOQRecordand children read by name through the genericRecordinterface; only the carrier-payload case (DML@mutationRecord(table)vs@service-on-MutationTableRecord(XRecord)for the same payload SDL Object) surfaces today. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R190 (
182ec24sealedGraphitronContext+ schema-drivenGraphitron.newExecutionInput(DSLContext, …)factory:ContextArgumentClassifierwalks everyMethodRef.Param.Typedwhose source isParamSource.Context, rejects mutually-incompatible Java types percontextArgumentname as a typedRejection.AuthorError.TypeConflict, and stores oneResolvedContextArg(name, javaType, sites)per name on a newClassificationcarrier;GraphitronContextInterfaceGeneratornow emitspublic sealed interface GraphitronContextwith a nestedpublic static final class GraphitronContextImpl implements GraphitronContext(same-compilation-unit permits, no javapoet permits surface needed) carrying a publicINSTANCEfield and a private constructor,getTenantIdremoved,getDslContextdemoted to a default readingenv.getGraphQlContext().get(DSLContext.class);GraphitronFacadeGenerator.newExecutionInputcollapsed to a single overload withDSLContext defaultDslfirst then one parameter perResolvedContextArgin alphabetical order, body null-checks every slot and populatesGraphQLContextwithDSLContext.class, each contextArgument string key, and the singletonGraphitronContextImpl.INSTANCEunderGraphitronContext.class; five DataLoader name emission sites de-prefixed (DataLoaderFetcherEmitter,TypeFetcherGenerator×2,MultiTablePolymorphicEmitter×2,QueryNodeFetcherClassGenerator);HandleMethodBodyfederation entity dispatch grouping collapsesMap<Integer, Map<String, List<Object[]>>>toMap<Integer, List<Object[]>>;graphitron-sakila-examplemigrated end-to-end (deletedAppContext.java,GraphqlResourcecallsGraphitron.newExecutionInput(dsl, "test-user"), 14 anon-impl test sites collapsed, twogetTenantId-override tests commented out with forward-reference to R45);8d9948614-page user-doc rewrite (getting-started.adoc,runtime-extension-points.adoc,runtime-api.adoc,test-your-schema.adocsubstantive rewrites;tenant-scoping.adoc,apollo-federation.adoc,split-vs-inline.adocdeferral banners pointing at R45; index + in-prose touch-ups acrosshow-it-works.adoc,batching-model.adoc,06-going-further.adoc,add-custom-conditions.adoc,security.adoc,graphitron-rewrite/docs/README.adoc);b408253L2ContextArgumentTypeAgreementTest(accepted + three-site conflict fixtures) + L4ContextArgumentTypeAgreementValidationTest(pins the validator-mirrors-classifier drain renders header + indented per-site lines and exposes the typedsitesfield);f1a6b7aL4GraphitronFacadeGeneratorPipelineTest(classified two-@service(contextArguments)-site SDL; asserts alphabetical parameter ordering, per-slotrequireNonNull, thegraphQLContextlambda body’s typed/string puts, theDataLoaderRegistryattach), L5 example SDL gains the single@service(contextArguments: ["userId"])site (Query.greetingByUser→UserGreetingService.greet), L6FilmContextArgumentRoundTripTest(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);0aa1ee7self-review pass addressing principles-architect findings ;Classificationcached onGraphitronSchemaas a 6th component populated once at parse boundary (validator + facade emitter both readschema.contextArguments()rather than re-classifying, restoring the "single producer" framing the load-bearing-classifier annotations promised), deadgraphitronContextCallparameter dropped fromDataLoaderFetcherEmitter.buildplus its threeTypeFetcherGeneratorcall sites,CallParamlifted to carryTypeName javaTypesoArgCallEmitter’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> expectedTypeslot dropped fromgetContextArgumentand 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 runtimeexpectedType.castwas redundant ceremony;Classification.resolvedswitched fromMap.copyOftoCollections.unmodifiableMap(new LinkedHashMap<>(…))so the alphabeticalTreeMapiteration 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,byTenantoverload, per-loader name partitioning, and the@tenantIddirective on top of a stable baseline. Out of scope (called out, all reserved for R45): tenant column Mojo config, tenant-scope classification,byTenantfactory overload, DataLoader name partitioning by tenant,@tenantIdARGUMENT_DEFINITION directive. Custom validator factory (<validatorFactory>Mojo element) reserved for R192. Follow-ups flagged by self-review, non-blocking: the singleton’sINSTANCEis 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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; 1870 graphitron tests + 347 example tests pass. -
R160 (
c2dc8d1C1-C3 sealedFieldClassification+TypeClassificationprojection families plusCatalogBuilderprojector switches landing onLspSchemaSnapshot.Built.{Current,Previous}symmetrically,d02a0bbC4-C5InlayHintsprovider with inferred-directive + classification arms plusInlayHintConfigandworkspace/didChangeConfigurationpush,039841bC6 classification hover via sealedDeclarationHoverparallel dispatch inHovers.compute,3d42eebC7docs/manual/reference/lsp-inlay-hints.adoc,f647ea5self-review pass 1 adding theworkspace/configurationinitialisation pull, theInferredDirectiveArgsindirection,MutationService.tableNameparity withQueryService, and aR160leak from the user-facing doc,366b07bself-review pass 2 co-locating projection-payload assertions insideGraphitronSchemaBuilderTest’s existing `// ===== <VariantName> =====classifier blocks via@ProjectionForplus theProjectionCoverageTestdrift-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 tofalseand live undergraphitron.inlayHints.inferredDirectives(ghost annotations at bare@table/@field/@referencesites showing the resolved value),graphitron.inlayHints.classification(compact label per field declaration and type declaration), andgraphitron.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. fourMutationField.DmlTableFieldpermits collapse to oneDmlMutation(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-sitterTreeonWorkspaceFile.tree()at request time (the AST asks "did the buffer carryname:?") rather than lifted onto the model ;Provenancediscriminators were attempted under a previous design and rolled back (~330 lines acrossTableRef,ColumnRef, the five@reference-permits,ParticipantRef.CrossTableField); the AST-read keeps the parse boundary closed and adds no model surface.BuildArtifactsshape unchanged: the newfieldClassificationsByCoord/typeClassificationsByNamefields live insideLspSchemaSnapshot.Built.{Current,Previous}symmetrically,Workspace.demoteSnapshotpreserves them, and stale-snapshot rendering mirrorsuserArgHover/columnHover’s "prefer stale info over silence" policy. C6 introduces a sealed `DeclarationHoverfamily (FieldDeclarationHover/TypeDeclarationHover) parallel to the directive-arg-keyedBehaviorfamily rather than wideningBehavior(the "Capability vs. sealed-switch confusion" principle:Behaviorstays directive-argument-binding-shaped, SDL declaration coordinates get their own resolver). Two new@LoadBearingClassifierCheckkeys (field-classification-payload-faithful,type-classification-payload-faithful) wear onCatalogBuilder.buildSnapshotwith matching@DependsOnClassifierCheckannotations on the three LSP consumers (inferred-directive arm, classification arm, classification hover). Tests: pipeline-tierGraphitronSchemaBuilderTestblocks gain@ProjectionFor-annotated sibling assertions running each canonical fixture through the projector and pinning the projected record type + payload values (24 new@Testmethods over ~20 variant blocks);ProjectionCoverageTestwalksGraphitronField/GraphitronTypesealed leaves and fails on any leaf without a@ProjectionForcover or a documentedNO_PROJECTION_REQUIREDexception; LSP-tierInlayHintsTestcovers config gating, the three inferred-directive arms, the classification arm, andBuilt.Previousstale rendering;DeclarationHoversTestcovers field-name and type-name cursor positions, theDmlMutationpayload shape, the directive-arg cursor short-circuit, andUnavailable/ missing-projection no-ops;GraphitronLanguageServerTestcovers the initialisation-timeworkspace/configurationpull;GraphitronWorkspaceServiceTestcovers the push-sidedidChangeConfigurationparse. 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 extendInferredDirectiveArgs.ENTRIESand 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,Unavailablemeans 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;InferredDirectiveArgslives in thecatalogpackage with its own string literals rather than underBuildContext’s `ARG_NAME/ARG_PATH, so a rename or new inference rule still maintains two places ; consolidate when a third consumer surfaces. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; 1869 graphitron tests + 318 graphitron-lsp tests pass. -
R191 (
2e84effinitial-import omnibus carrying spec + implementation + tests): honor@field(name:)for accessor lookup on free-form@recordparents on the table-bound and polymorphic-hub branches.FieldBuilder.collectAccessorMatchestakes anaccessorBaseNameparameter and matches against<base>/get<UcBase>/is<UcBase>instead of the SDL field name;deriveAccessorRecordParentSource,resolveRecordParentSource, andderivePolymorphicHubSourcethread the value from their callers (fieldNameretained for cardinality-mismatch text that quotes the SDL name). TheTableBoundReturnTypearm atFieldBuilder.java:3700reuses the already-computedcolumnName;classifyRecordParentPolymorphicChildreads@field(name:)at:4327-4329before dispatching toresolvePolymorphicRecordParent. Theaccessor-rowkey-shape-resolvedandaccessor-rowkey-shape-resolved-against-hub@LoadBearingClassifierCheckdescription blocks each gain a sentence: matched accessor’s name is the directive value when present on a free-form@recordparent, else the GraphQL field name. TheAccessorRef.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 (resolveRecordAccessoralready threaded the directive value asaccessorBaseName). Coverage: pipeline-tierGraphitronSchemaBuilderTest.AccessorDerivedSourceCaseaddsACCESSOR_ROWKEYED_FIELD_NAME_REMAPS_ACCESSOR(admit onRemappedPayloadwith@field(name: "filmRecord"), assertsRecordTableField+AccessorCall.accessor().methodName() == "filmRecord"+ cardinalityONE) andACCESSOR_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_fieldNameRemapsAccessorcovers the polymorphic-hub admit (ListPayloadparent, SDL fieldreferrerswith@field(name: "films"), asserts hubfilm+AccessorCall.methodName == "films"+ cardinalityMANY). NewAccessorPayloads.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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-db -P!docson Java 25. -
R194 (
5e5f5e3builder pass + 5 pipeline cases,a1feaceself-review:EmitsPerTypeFilecapability + typedCaseFoldCollision+ 2 cases,f69b479preserveCaseFoldCollisionunderprefixedWith): rejects case-insensitive type-name collisions at build time.GraphitronSchemaBuilder.rejectCaseInsensitiveTypeCollisionsruns post-ConnectionPromoter.rebuildAssembledForConnections(rather than post-promote, so the assembledGraphQLSchematypeRefs stay resolvable when a synth Connection is demoted) and case-folds viaLocale.ROOT; every member of each case-equivalent group demotes toUnclassifiedTypecarrying a typedRejection.InvalidSchema.CaseFoldCollision(group, origin, prefix)withOrigin∈{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);validateUnclassifiedTypeprojects oneValidationErrorper member. The emit-vs-no-emit split is lifted out ofGraphitronTypeonto a newEmitsPerTypeFilecapability marker (mirrorsSqlGeneratingField/BatchKeyField), implemented by every variant exceptScalarTypeandUnclassifiedType; detector filters viainstanceof EmitsPerTypeFile.prefixedWithreturns a same-variantCaseFoldCollisionwith accumulated prefix rather than degrading toStructural, satisfying R58’s typed-rejection-preserved-under-wrap contract (the validator’sprefixedWith("Type 'X': ")is the only path that reachesValidationError.rejection). Coverage:GraphitronSchemaBuilderTest.CaseInsensitiveTypeClashCaseparameterised 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.prefixedWithPreservesCaseFoldCollisionTypedFieldspins single + re-prefixed paths;RejectionSeverityCoverageTest+SealedHierarchyDocCoverageTestpick up the new permit;typed-rejection.adoccarries the prose + mermaid-class entry. Out of scope (called out): legacyMakeConnections/graphitron-schema-transformclassifier, auto-mangling colliding names, federation cross-subgraph clashes, derived-filename collisions beyond the type-name stem. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R196 (
c42ed64route Workspace recalc through listener seam): lifts "drain follows enqueue" from author-discipline at three of sixWorkspacepublic mutators (didOpen,didChange,didClosepaired with explicitpublishDiagnosticsForRecalculate();setBuildOutput,demoteSnapshot,markAllForRecalculationunpaired and reachable fromDevMojo.regenerate/rebuildCatalogafter schema-file + classpath watcher events) to a structural invariant across all six. New privateenqueueAndNotify(Runnable)helper performs the queue mutation underlockand fires a single-slotvolatile Runnable recalculateListenerafter lock release; the six public mutators route through it (setBuildOutput/demoteSnapshottransitively viamarkAllForRecalculation), sotoRecalculatewrites only happen inside the funnel.GraphitronTextDocumentService.setClientregistersthis::publishDiagnosticsForRecalculateas the listener; the explicit publish calls indidOpen/didChange/didCloseare 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 `lockthrough the heavyDiagnostics.computebody; idempotency on the drain side (a seconddrainRecalculateafter 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-tierWorkspaceTest.everyPublicQueueMutatingMethodFiresTheListenerparametrises over the six mutators asserting listener-fire count delta of exactly 1;recalculateListenerDefaultsToNoOpForTestHarnessespins that mutators on a workspace withoutsetRecalculateListenerdo not NPE;drainRecalculateIsIdempotentOnEmptyQueuepins the single-extraction property the listener path depends on;demoteSnapshotOnNoOpDoesNotFireListenerparametrises 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-tierBuildTriggerPublishesDiagnosticsTestcapturespublishDiagnosticscalls on a stubLanguageClientand 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-sidebuildOutput()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 (sealedRecalculateEventdiscriminating editor / build / demotion causes ; drain is cause-agnostic, sub-taxonomy carries no information consumers act on differently); non-DevMojocallers ofmarkAllForRecalculation/demoteSnapshot/setBuildOutput. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R197 (
a755e39wire LSP didSave to in-process regen trigger,30473f9In Progress → In Review): wiresGraphitronTextDocumentService.didSaveto aConsumer<String> onSchemaSavedlistener constructor-injected viaGraphitronLanguageServerand propagated throughDevServerfromDevMojo.DevMojo.buildSaveListener(suffixes, debounce, regen)(package-private static) filters URIs byRewriteContext.schemaFileExtensions()and schedulesregenerate(workspace)through the sameschemaDebouncethe FS watcher uses, so editor saves and watcher events coalesce on a single regen.DebounceExecutorconstruction hoisted fromstartSchemaWatcherup intoexecute()so the listener can be built beforebindServer. Headless LSP-only use sites (standaloneLauncher, existingTextDocumentServiceTestfixtures) keep their behaviour via no-arg / one-arg constructor defaults that pass a no-opConsumer.Workspaceis 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_invokesListenerWithUripins the URI-typed listener contract,didSave_noopWhenListenerAbsentpins the headless contract,DevMojoTest.saveListener_schemaSuffixSchedulesRegencovers the suffix filter and debounce scheduling. Docs:getting-started.adocdev-loop prose calls out the dual-path model (LSP didSave primary, FS watcher headless fallback) and the Mermaid diagram gains the LSPdidSave -> dispatcharrow;DevMojoclass-level javadoc updated to match. Out of scope (called out, not regressed):didChangeregen (save is the user’s intentional commit point; mid-typing buffers are partial SDL); replacingSchemaWatcher(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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R198 (
b39d1c5lift SchemaWatcher FS-bound tests to synthetic dispatch,e10785cself-review: package-private dispatch + run()-time polling hint): lifts the fiveSchemaWatcherTestcases that depended on real-FS event delivery (modifyingGraphqlsFile_firesCallback,deletingGraphqlsFile_firesCallback,rapidWrites_firesCallbackOnce,newSubdirectory_isRegisteredAndFiresCallback, and oneCatalogRefreshTestcase) onto syntheticWatchEventvalues driven directly intoSchemaWatcher.dispatch. macOS’s JDK shipsPollingWatchServicewith a hardcoded 10 s period (sinceSensitivityWatchEventModifierwas 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 ondispatch(suffix filter, OVERFLOW reschedule, on-the-fly subdirectory registration) dressed up as integration tests of the JDK’s WatchService.writingGraphqlsFile_firesCallbacksurvives as the Linux-only inotify smoke (@EnabledOnOs(LINUX));nonGraphqlsFile_noCallbackdeleted as a duplicate ofdispatch_ignoresUnconfiguredSuffix;graphqlsWriteDoesNotFireClasspathWatcherfolded into the synthetic shape. NewwatchServiceBackend_matchesExpectedPerOsprobe pinsPollingWatchServiceon macOS andLinuxWatchServiceon 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 ofSchemaWatcher.run()emits twoLOGGER.infolines 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.dispatchstays package-private; the lone cross-package consumer (CatalogRefreshTestin..maven.dev) routes through a new test-onlyDispatchTestSupportclass undersrc/test/java/.../maven/watch/, matching the existingwatchedDirs()test-seam precedent. Test counts:SchemaWatcherTest11→11,CatalogRefreshTest2→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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R189 (
666f0fbadmit FK-target @nodeId input fields on @mutation,9524885In Progress → In Review): admitInputField.ColumnReferenceField/CompositeColumnReferenceField(FK-target@nodeId(typeName: T)pointing at another@table’s NodeType, classified to `Resolved.FkTarget.DirectFk) on every non-UPSERT@mutationverb. The reference carriers'liftedSourceColumnslive on the input’s own table, the extraction is narrowed toCallSiteExtraction.NodeIdDecodeKeys, and the emitters bind decoded keys againstliftedSourceColumnspositionally ; the same shape the same-tableColumnField/CompositeColumnFieldNodeId carriers already drive.MutationInputResolver.resolveInputdrops 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.LookupKeyFieldandInputField.SetFieldwiden permits to include both reference carriers. Load-bearing:EnumMappingResolver.buildLookupBindingsaddscase ColumnReferenceField/case CompositeColumnReferenceFieldarms emittingMapGroup/DecodedRecordGroupoverliftedSourceColumns(); without this themutation-input.where-columns-cover-pkcheck would silently under-count reference contributions and fire false "missing PK column" rejections on schemas whose FK column covers the PK.TypeFetcherGeneratorextracts five new helpers (emitSetMapPuts,emitSetExcludedPuts,emitSetVColNameAdds,emitSetBulkCellAdds,emitSetVFieldPutsplussetFieldColumns/setFieldNodeIdExtractiondispatchers) that replace eight(InputField.ColumnField) sfcasts overtia.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@LoadBearingClassifierCheckannotations are restated:mutation-input.where-columns-cover-pk(semantic ; filter-column contributions now includeliftedSourceColumns()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-tierMutationDmlNodeIdClassificationTestadds eight R189 cases ; arity-1 INSERT admission, arity-1 DELETE PK-coverage, arity-1 UPDATE with@valueSET field, composite-key DELETE throughreordered_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), andfkTargetNodeIdRef_upsert_stillRejected_underR144(UPSERT refusal at the kind gate supersedes admission). Out of scope (called out, not regressed):Resolved.FkTarget.TranslatedFkadmission (the parent_node + child_ref shape where the FK targets a non-PK NodeType keyColumn ;BuildContext.java:1846-1849continues to produceInputFieldResolution.Unresolved); UPSERT (R145 territory); the output-side JOIN-with-projection NodeId encoding R24 originally hand-off pointed at. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R187 (
8e70b8cIn Review: nested @service arg-mismatch diagnostic at non-SOURCES shapes): the unresolved-@service-parameter discriminator inServiceCatalognow selects the arg-mismatch arm whenever the parameter type is not SOURCES-adjacent, instead of gating that arm onparentPkColumns.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 +argMappingsuggestion. The DTO-shape rejection arm is gated to nested coordinates only (!parentPkColumns.isEmpty()), preserving the root +List<DTO>→ arg-mismatch precedence pinned bydtoSources_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-tierServiceCatalogTestrewritesreflectServiceMethod_unrecognisedParam_onChildField_*(now asserts arg-mismatch ongetWithUnknown(Object)under non-emptyparentPkColumns) and addsreflectServiceMethod_nonSourcesPayloadOnChildField_pointsAtArgCtxMismatchpinning theLocalDatereproduction. Pipeline-tierGraphitronSchemaBuilderTestaddsSERVICE_ON_CHILD_WITH_NON_SOURCES_PARAM_NAME_MISMATCH_REJECTEDalongside 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 sealedUnresolvedParamclassifier would consolidate the precedence in one place. -
R185 (
b6539b9): narrowServiceCatalog.looksLikeSourcesShapetoList<RowN>/List<RecordN>only. A root@servicewhose Java parameter is aList<XRecord>(a concreteTableRecordsubtype) under a name that doesn’t match any GraphQL argument was getting the "`@service` at the root does not supportList<Row>/List<Record>/List<Object>batch parameters" diagnostic, shadowing the actionable arg-mismatch diagnostic that lists available argument names and suggestsargMapping.List<XRecord>at root is the canonicalInputBeanResolvershape, so a plain name typo collided with the Sources-shape exception. The concreteTableRecordbranch is removed fromlooksLikeSourcesShape; 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-tierSERVICE_AT_ROOT_WITH_TABLERECORD_PARAM_NAME_MISMATCH_REJECTED(GraphitronSchemaBuilderTest) asserts the arg-mismatch diagnostic wins forList<FilmRecord>under a mismatched name; the two existingRowN-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 (
3ccd1eaGitLab pipeline targets graphitron-rewrite reactor on tags,bf5d2c3provision postgres service + run codegen + tests in publish,40790fcapt-get gcc so graphitron-lsp’s native build works,236860fparameterize test.db.url so CLI -D reaches surefire,dcdf0d2self-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:snapshotis deleted outright; default-branch pushes (including GitHub → GitLab mirror sync) no longer fire any deploy, eliminating the active hazard of legacy9-gitlab-SNAPSHOTartifacts accumulating in the Sikt GitLab Packages registry on every mirrored commit.publish:releasenow runsmvn -f graphitron-rewrite/pom.xml versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false -DprocessAllModules=true(theprocessAllModulesflag is load-bearing: without it the aggregator pom updates but child modules keep${revision}and the deploy publishes mismatched coordinates) followed bymvn -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 newgitlabprofile ingraphitron-rewrite/pom.xmldeclares the GitLab Packages<repository>(no<snapshotRepository>; the rewrite parent’s invariant pergraphitron-rewrite/docs/README.adocPublishing: an accidentalmvn deployon10-SNAPSHOTmust fail fast) plusdeployAtEnd=trueand the sources-jar attachment; the root-pom legacygitlabprofile is unreachable from the new caller and dies with the legacy reactor under R182. The pipeline image bumps frommaven:3.9-eclipse-temurin-21tomaven:3.9-eclipse-temurin-25to satisfy the parent pom’srequireJavaVersionenforcer rule. In-runner Postgres:publish:releaseprovisions apostgres:18-alpineservice (aliaspostgres,POSTGRES_HOST_AUTH_METHOD=trust),apt-get install`s `postgresql-client+gccin the runner, and appliesgraphitron-rewrite/graphitron-sakila-db/src/main/resources/init.sqlviapsql. Thelocal-dbprofile is activated alongsidegitlabso jOOQ codegen reads against the live service rather than spinning up a Testcontainer (no Docker-in-Docker). Thegccinstall coversgraphitron-lsp’s `build-native.shinvocation atgenerate-resourcesthat compiles the tree-sitter native lib bundled into the publishedgraphitron-lspjar atlib/linux-x86_64/libtree-sitter-graphql.so. Surefire parameterisation:graphitron-sakila-example’s `local-dbprofile liftstest.db.url/test.db.username/test.db.passwordfrom 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 triedlocalhost. 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 onmainis 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-pomgitlabprofile (R182 collapses both); Maven Central publishing onmain(separate fix once R182 retires the legacy reactor; this item only touches GitLab); cleanup of existing junk9-gitlab-SNAPSHOTartifacts already deposited (manual GitLab UI task); arelease-clijob 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-parentpom,no.sikt:graphitron-javapoet,no.sikt:graphitron,no.sikt:graphitron-maven-plugin,no.sikt:graphitron-lsp) while the sixmaven.deploy.skip=truemodules stay out of the registry, and a default-branch push triggers no publish job. Build green locally: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 with thelocal-dbprofile pointing at a native Postgres. -
R178 (
9ee35d9step 1 DML-only cutover,e41ddb4step 2a wire transportForParent through selectErrorsTransport,64dfa33step 2b @service-carrier classifier wiring,fe270f9step 3 retire SettKvotesporsmal-bug carrier-walk consultations,c44193bPhase 4 slice 1 delete dead writers,7cbe4b6slice 2 retire transitional consultations,1211e6dslice 3 non-DELETE structural detection,e665e11slice 4 DELETE arm structural detection,1e71906slice 5 lift structural carrier scan to BuildContext,d0de975slice 6 retire verbless walk’s carrier-walk consultation,475e2ecslice 7a retire carrier-walk methods + load-bearing re-anchors,a1bbbcdslice 7b retire sealed model types,e06f1c8slice 7c retire SingleRecordIdentityField permit,83d2182slice 7d cosmetic,4a67b26self-review cleanup,1ddb22brename "Carrier" identifiers to "Payload",f42b819final self-review,575cd9bIn Progress → In Review): collapses the parallel single-record carrier walk to the unifiedSourceKey+ R96 reflection path. Deletes seven sealed hierarchies (SingleRecordCarrierResolution,SingleRecordCarrierShape,CarrierFieldRole,DataElement,BuildContext.tryResolveSingleRecordCarrieroverloads +classifyCarrierField,BuildContext.carrierProducerRegistry, the fourregister*CarrierDataFieldwriters,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 siblingProducerBindingarms (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 resultBuildContext.DmlPayloadScan(Admit(dataField, DmlElementKind) | Reject(reason) | NotApplicable) replaces the carrier walk for the @mutation classifier andMutationInputResolver. The@fielddirective on a non-$sourcepayload data field no longer hard-rejects ; the SettKvotesporsmal contract pin: with and without@field(name:), semantically identical schemas classify identically. Themutation-dml-record-field.data-table-equals-input-tableinvariant re-anchors on the smallerrequireDmlDataTableMatchesInputTablehelper covering both DELETE and non-DELETE arms; theerror-channel.local-context-transportinvariant re-anchors onFieldBuilder.detectStructuralDmlErrorChannel(the new sole producer ofErrorChannel.LocalContexton DML payloads). The unit-tierErrorsTransportSelectionTestpins the errors-field defaulting rule table; the pipeline-tierSettKvotesporsmalShapeRegressionTestpins 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-tierErrorsTransportSelectionTest(8 cases pinning every branch ofFieldBuilder.selectErrorsTransport); pipeline-tierSettKvotesporsmalShapeRegressionTest(3 cases: with-@fieldadmits, without-@fieldadmits identically, ClassBacked return-mismatch diagnostic cites the payload class not the inner record); pipeline-tierSingleRecordPayloadPipelineTest(renamed fromSingleRecordCarrierPipelineTest, 33 cases including R178 admits of@fieldand@deprecatedon the data field); execution-tierSingleRecordPayloadDmlTest(renamed, durability pins intact); theMUTATION_DML_RECORD_FIELD/MUTATION_BULK_DML_RECORD_FIELD/ DELETE-carrier /SINGLE_RECORD_IDENTITY_FIELD_ORPHANrows inGraphitronSchemaBuilderTestretarget to the unified path. Phase 5 deferral (called out in spec, separable follow-up): the emit-side migration toWrap.Row+Reader.ColumnReadships under R180 (record-parent-column-read-helper); Phase 5 survivors (ChildField.SingleRecordTableField,*FromReturning,Reader.ResultRowWalk, the fiveFetcherEmitter.buildSingleRecord*methods, three remaining load-bearing keys) stay alive on the producer side until R180 lifts them. Out of scope (called out, not regressed):RecordBindingResolverwalk 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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R184 (
05e21d5Graphitron.newExecutionInput factory + getContextArgument default,5d308c2In 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 emittedGraphitronfacade.GraphitronFacadeGeneratornow emits two staticnewExecutionInputoverloads alongsidebuildSchema:newExecutionInput(GraphitronContext context)returnsExecutionInput.newExecutionInput().graphQLContext(b -> b.put(GraphitronContext.class, context)).dataLoaderRegistry(new DataLoaderRegistry()), and the single-tenant conveniencenewExecutionInput(DSLContext dsl)delegates tonewExecutionInput((GraphitronContext) env -> dsl). To make the lambda form bind toGraphitronContext’s SAM rather than infer as `Function<DataFetchingEnvironment, DSLContext>,GraphitronContextInterfaceGeneratorflipsgetContextArgumentfromABSTRACTtoDEFAULTwith bodyreturn env.getGraphQlContext().get(name);(matching the legacyDefaultGraphitronContext);getDslContextis 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-tierGraphitronFacadeGeneratorTestasserts the twonewExecutionInputoverloads exist with(GraphitronContext)and(DSLContext)parameter lists, both returninggraphql.ExecutionInput.Builder, bothpublic static; pipeline-tierGraphitronContextInterfaceGeneratorTestassertsgetContextArgumentcarriesDEFAULT(notABSTRACT) with the right body and that the emitted interface has exactly one abstract method; compilation-tierNewExecutionInputFactoryTestingraphitron-sakila-examplepins 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 typedGraphitronContext.classkey generated fetchers read from). Execution coverage comes from the sakila example’s rewrittenGraphqlResource.execute: the six-line builder collapses to three viaGraphitron.newExecutionInput(new AppContext(...)), and every existing execution test ingraphitron-sakila-exampleruns through that method. Docs updated to point at the factory (getting-started.adochello-world + multi-tenant + DataLoader-registry sections;runtime-extension-points.adocregistration snippet; emitted facade + interface javadocs). Out of scope (called out, not regressed): no federation overload ofnewExecutionInput(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 viacomputeIfAbsent); no new module, no change to dependency graph;getTenantIdandgetValidatordefaults unchanged (they were already default-method shaped). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R179 (
ef0af6cdelete ResultAssembly; service success arm is universal passthrough,a3bde69self-review cleanup: retire stale ResultReturnType coverage claim): deletes theResultAssembly+ResultSlotmodel types, theresolveServiceResultAssembly+buildResultAssemblyBeanArmclassifier inFieldBuilder, and thebuildSuccessPayload/buildSuccessPayloadCtor/buildSuccessPayloadSettersemit helpers inTypeFetcherGenerator. 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-declaredCreateFilmPayload { film: Film, errors: [Error] }does not need a Java twin. The catch-armpayloadFactoryLambdais 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-backedFieldrecords (Query/Mutation × Table/Record) lose their trailingOptional<ResultAssembly> resultAssemblycomponent; the four arms inclassifyQueryField/classifyMutationFieldroute throughbuildServiceField, whoseBiFunction<channel, assembly, ...>collapses toFunction<channel, ...>. A new surviving classifier check (checkServiceReturnMatchesPayloadinFieldBuilder) 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 (FieldBuilderLoadBearingClassifierCheck descriptions ×2 +ServiceCatalog.reflectServiceMethoddescription retired ResultReturnType coverage claim,FieldBuilderjavadoc ×4, model-class javadocs onDefaultedSlot/PayloadConstructionShape/NonBoundSetter/ErrorsSlot,ServiceDirectiveResolver/MutationInputResolverResultReturnTypearm comments,SettKvotesporsmalShapeRegressionTest+TestServiceStubjavadoc). 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)returningSakPayload, replacing the String-returning stub that the pre-existingFetcherPipelineTest.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.LoadBearingGuaranteeAuditTestnet stays balanced: bothpayload-construction.*producers retain surviving consumers (catch-armpayloadFactoryLambda+ validator pre-stepdeclareEarlyPayloadFromErrors/declareEarlyPayloadSetters); no producer becomes orphaned. Out of scope (called out, not regressed): inlining the success-arm local (kept for catch-armtry-block uniformity); redesigning the catch-armpayloadFactoryLambdapath (the only remaining DTO-construction site, principled and unchanged). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R177 (
1aba97fchild @service rows-method preserves specific XRecord type,de5ed05self-review: name the third consumer in RowsMethodShape class doc): narrows the child-@serviceemit site so the rows-method’sVand the matchingDataLoader<K, V>value type both honor theTableBoundReturnTypeclassifier guarantee instead of widening to raworg.jooq.Record.RowsMethodShape.strictPerKeyTypereturnstb.table().recordClass()for theTableBoundReturnTypearm (was: aRECORDconstant, now deleted);TypeFetcherGenerator’s `ChildField.ServiceTableFieldarm threadsstf.returnType().table().recordClass()as the singleservicePerKeyTypelocal into bothbuildServiceDataFetcher(theDataLoader<K, V>typing line) andbuildServiceRowsMethod(theMap<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 `@LoadBearingClassifierCheckdescription is rewritten to name both emit-site consumers explicitly (rows-method.returns(...)and the typedDataLoader<K, V>), capturing that the strictTypeName.equalsarm is now load-bearing for the typed loader’s compile via Java generics invariance ; not just structural symmetry; a new@DependsOnClassifierCheckonbuildServiceDataFetcherpins the data-fetcher side to the same key, and the existing annotation onbuildServiceRowsMethodis 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>'tomust return 'List<LanguageRecord>'(the specific record class for the field’s bound table). Tests: sixServiceTableFieldunit assertions inTypeFetcherGeneratorTestflip fromorg.jooq.Recordto the specificFilmRecord(positional + mapped, single + list, on both data-fetcher and rows-method return); three new R177-axis enum rows inGraphitronSchemaBuilderTestpaired with three new fixtures inTestServiceStubcover 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.getFilmstightened toList<List<FilmRecord>>with the paired pipeline assertion flipped; new positive compile-tier fixtureFilm.languageByServiceingraphitron-sakila-examplebacked byFilmService.languageByServicereturningMap<Record1<Integer>, LanguageRecord>makesmvn compile -pl :graphitron-sakila-examplethe 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 ;ServiceRecordFieldcarries the broadReturnTypeRefsealed root rather thanTableBoundReturnType, andstrictPerKeyTypecan return null for that variant); theSourceKey.Wrap.TableRecordsource-side typing pipeline (R177 brings the target side into alignment with what the source side already does); the rows-method’s outer container shape (MapvsList, single vs list cardinality ;outerRowsReturnTypecontinues to wrap whateverperKeyit’s handed);SplitRowsMethodEmitter(theChildField.SplitTableFieldarm stays rawRecord, explicit non-goal). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R96 (
ab18e8cProducerBinding sealed taxonomy + RecordBindingMismatch rejection,d098d76RecordBindingResolver ; reflection-driven SDL → backing-class walker,bc1e457wire RecordBindingResolver into TypeBuilder + reflection-derived dispatch,2a19d40transitional dispatch: gate result-axis walker on@record+ directive fallback,37ee435flatten RecordBindingMismatch to RecordBindingMultiProducer + doc coverage,4127f3dpipeline-tier tests for record-binding behaviour,ec229f1self/arch review fixes): replaces the directive-drivenrecordBackingClassespopulation inTypeBuilderwith a reflection-driven walk that grounds at root producers (@servicereturns,@tableresolutions,@tableMethodreturns) 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 agreedClass<?>(singleton) orRejection.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 optionalDataFetchingEnvironmentparam), 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 asIllegalStateException. The new sealedProducerBindingtaxonomy (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’sclassNameequals the reflected class or the directive carries noclassName;Disagrees("graphitron derives <X>") when they differ, naming the reflected class;Shadowed by @table(input types only) when@tableco-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 + @recordinput-side warning atTypeBuilder.java:826-831is removed; the redundancy signal is now carried by theShadowed by @tablevariant. Load-bearing pin: producer@LoadBearingClassifierCheck(key = "record-binding.producer-agreement")onRecordBindingResolver(description names the two pure-function commitments riding under the check:ServiceCatalog.resolveTableByRecordClassderivesTableRefpurely fromcls, and a Java record’s component list is a pure function ofcls) pairs with@DependsOnClassifierCheckonFieldBuilder.resolveRecordAccessor. Tests: pipeline-tierR96RecordBindingPipelineTest(six cases: Matches with@serviceproducer; 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’sPojoResultType.NoBackingpromotion; multi-producer disagreement surfaces typedRecordBindingMultiProducerrejection);RejectionSeverityCoverageTestadds the new permit to its sample factory;GraphitronSchemaBuilderTest’s `TABLE_PLUS_RECORDandSERVICE_WITH_RECORD_BACKING_CLASS_MISMATCH_REJECTEDcases update to assert R96 semantics (table wins on input + Shadowed-by-table warning; service-with-record-mismatch corrects silently with Disagrees warning instead ofUnclassifiedField);ErrorChannelClassificationTest.unTypedRecordPayload_*flips from "produces no channel" to "produces channel from reflected producer" ; a correctness improvement R96 introduces because@recordwithoutclassNamenow grounds via the producer’s return type rather than falling through toNoBacking;SealedHierarchyDocCoverageTestpasses against the updatedtyped-rejection.adoc(new fourthAuthorErrorarm 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’sclassNamematches 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@serviceresult-axis observation is gated on the SDL return type carrying@record(preserves R75 single-record-carrier semantics; the post-retirement anchor isBuildContext.tryResolveSingleRecordCarrier, and the follow-on must flip the gate atomically with directive retirement);@tableMethodarm contributes input-axis observations only (the@tableobservation alone is sufficient for the result axis; obtaining theTable<Record>reflection class viaTableImpl.recordType()for bare-class returns would add machinery without strengthening the diagnostic);buildResultType/buildNonTableInputTyperetain a directive-className fallback for types the walker can’t reach (so existing fixtures classify without mass migration); the rejection lands as a singleRejection.AuthorError.RecordBindingMultiProducerpermit directly underAuthorErrorrather than a two-levelRecordBindingMismatch.MultiProducersub-taxonomy (the flat shape matches the rest of theAuthorErrorfamily and the typedList<ProducerBinding>payload already carries what a sub-arm would key off; theSealedHierarchyDocCoverageTest’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:290directive 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 (recordShapeslot on the fourInputTypepermits is graphitron-emitted validation class; R96’srecordBackingClassesis author-supplied accessor target ; orthogonal axes). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R176 (
8292468preserve upstream rejection in EntityResolutionBuilder): replaces the misleading@key requires a @table-bound type; '<T>' has no @table directivesynthesised atEntityResolutionBuilder.java:108-114for 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. WhengType instanceof UnclassifiedType(the type was already rejected upstream byTypeBuilder.unknownTableRejection, the@nodekeyColumnsunresolved-column check, malformedKjerneJooqGeneratornode-id metadata, or@nodedeclared on a type withoutimplements Node), the new code skips the demote outright and letsGraphitronSchemaValidator.validateUnclassifiedTypesurface the original cause unmodified. WhengTypeis a genuine non-table-bound classification surviving theassembledType instanceof GraphQLObjectTypeandTableInterfaceTypepre-checks (today:PlainObjectTypeand theResultTypesub-hierarchyJavaRecordType/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 privatekindLabel(GraphitronType)switch (PlainObjectType→"a plain object type", the fourResultTypevariants →"a @record type", default branch →"a non-table-bound type"for any future classification a contributor adds without updating the switch). The three legitimatedemotecallers (TypeBuilder.java:226typeId collision,EntityResolutionBuilder.java:104@keyonTableInterfaceType,EntityResolutionBuilder.java:128alternative-build error) all demote from classified entries and stay as-is; only the bug call site is gated. Why call-site, not aTypeRegistry.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:EntityResolutionBuilderTestadds three regression cases pinning the new behaviour (keyOnTypeWithUnresolvableTable_preservesUnknownTableRejectionasserts the rejection containscould not be resolved in the jOOQ catalogand does not containhas no @table directive;keyOnNodeTypeWithUnresolvableKeyColumn_preservesUnresolvedColumnRejectionasserts the rejection containskey column 'definitely_not_a_column' in @node could not be resolvedand the same absence;keyOnRecordType_namesRecordKindInMessageasserts the rejection containsis classified as a @record typeand the same absence), and tightens the existingplainObjectTypeWithKey_demotesToUnclassifiedTypefrom a loosecontains("@table")to require bothis classified as a plain object typeandfederation 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-UnclassifiedTypepolicy is unchanged); LSP fix-it hints for the new wording (the structuralRejectioncarries enough payload for an LSP layer to consume later); changingTypeRegistry.demotesemantics. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R94 (
1224167scope down emit shape to class-not-record (R172/R174 follow-ons filed),418ef97ship per-SDL-input-type validation class + rewired validator pre-step,e86c856self-review cleanup: drop dead two-argInputRecordGenerator.generateoverload, simplify nested-inputfromMapto one statement, refresh "Java record" → "Java class" Javadoc with R174 forward-looking notes): emits one graphitron-internal Java class per reachable SDLinputtype at<outputPackage>.inputs.<InputName>and rewires R12’s validator pre-step atTypeFetcherGenerator:1602+to walk the typed instance instead of the rawMap. NewHasInputRecordShapecapability interface declared on the fourInputTypeleaves (PojoInputType,JavaRecordInputType,JooqRecordInputType,JooqTableRecordInputType) and onTableInputType;InputRecordShapecarries(recordClass, List<InputComponent>)with a compact constructor that rejects null/empty and backs theinput-record.shape-from-input-type@LoadBearingClassifierCheckkey.TypeBuilder.buildInputRecordShapewalks SDL fields and lifts scalars via R101’sScalarTypeResolver, enums toString(graphql-java’s wire shape), nested input refs toClassName.get(<outputPackage>.inputs, name)(forward-declared ; javapoet does not require the class to exist at codegen, so mutually recursive inputs resolve cleanly), and lists toList<X>; a field whose scalar fails to classify routes the parent throughUnclassifiedTypevia the existing fail-mode.InputRecordGeneratorwalks the reachable input closure off the assembledGraphQLSchema’s `GraphQLObjectTypefields (the rewrite model’sRootType/TableBackedTypedon’t carryschemaType(), so the assembled schema is the authoritative source), expanding transitively through nested input components; each emitted class ispublic finalwith one private field per SDL component, a public same-name accessor, a private canonical constructor, a staticfromMap(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 explicitnullboth collapse to a null component), and a per-class Javadoc tagging it as a graphitron-internal validation target."inputs"is added toGraphQLRewriteGenerator.OWNED_SUBPACKAGESso the orphan sweep cleans regressions.TypeFetcherEmissionContextgrowsassembledSchema()+parentTypeName()so the rewired validator pre-step inTypeFetcherGeneratorcan 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 feedvalidator.validate(<typed>). Scalar/enum args stay on the raw value path. Class-not-record by deliberate scope-down:graphitron-javapoetdoes not currently supportTypeSpec.Kind.RECORD,sealed/permitsclauses, orpackage-info.javaemission (covers onlyCLASS,INTERFACE,ENUM,ANNOTATION); R174 (javapoet-record-sealed-package-info-support, Backlog) tracks the framework upgrade ; once it lands,InputRecordGeneratorre-emits as actual records + sealed marker + package-info with no model-side ripple (InputRecordShape/InputRecordGeneratorkeep their names and semantics, only the renderedTypeSpecshape 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’sConstraintViolations.toGraphQLErrorneeds, 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")onTypeBuilder.buildInputRecordShapepairs with@DependsOnClassifierCheckonInputRecordGenerator(per-input-type class emission) andTypeFetcherGenerator.validatorPreStep(typed-record materialisation in the pre-step rewire). Tests: pipeline-tierFetcherPipelineTestadds 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 ; andinputRecord_validatorPreStep_receivesTypedRecordNotMap; the regression guard against drifting the pre-step back tovalidator.validate(Map)); unit-tierInputRecordGeneratorTestcovers the emit shape (public class in<outputPackage>.inputs,fromMapfactory withMap<String,Object>signature, one accessor per SDL component);InputTypeValidationTestgains aplaceholderShape()helper since it constructsPojoInputTypedirectly without the classifier. The validator pre-step walks the empty record (no constraints attached yet) ; the shape of the record (components,fromMapsignature, walk-target) is exercised end-to-end on every fetcher with an input arg, so R98’s later content-attachment (programmaticConstraintMappingentries) 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 mergedConstraintSet’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-tierConstraintViolationround-trip fixture. Out of scope (called out, not regressed): exposing emitted classes to service signatures (R150 owns@servicevalue flow via consumer-authored beans; the graphitron class is a validation target only and is discarded aftervalidator.validatereturns); replacing theMap.get()pattern in DML emitters (the fourbuildMutation{Delete,Insert,Update,Upsert}Fetcherpaths 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@servicecallsites (R150 owns); service-sidevalidator.validatecalls (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@recordonINPUT_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@recordbinds the input type to); retiring the fourGraphitronType.InputTypevariants (R96 keeps the variants and reshapes how they’re populated); the@table + @recordshadow rule atTypeBuilder:815-824(untouched). Forward references: R164 (field-model-two-axis-pivot, Backlog) will repoint the validator-pre-step dispatch site intoValidationBuilder.OnInput-arm pattern matching once the field-model pivot lands; the substance of the pre-step (call<InputName>.fromMap, thenvalidator.validate) is unchanged ; R94’sInputRecordShape/InputComponentare type-side (attached toGraphitronType.InputType), andValidationBuilderis field-side (attached toField), so the two axes don’t compete. R171 (input-like-type-sealed-parent, Backlog) tracks foldingInputType ∪ TableInputTypeunder a sealedInputLikeTypeparent so the capability declaration becomes one site instead of five; until R171 lands, a future sixth input-like variant added toGraphitronType.permitswill not get a compile-time miss forHasInputRecordShape. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 (1763 graphitron module tests + sakila compile-tier + execute-tier). -
R9 (
a4675bfPhase 1 pipeline,c38ea0fPhase 2 in-repo .md → .adoc,562e732Phase 3 alf/graphitron-landingsside absorption,aa3511ePhase 4 roadmap/by-theme/changelog/plans render,7abec54Phase 5a custom-domain cutover,b824207In Review):graphitron.sikt.nois now built by Maven and deployed to GitHub Pages./docs/is apom-packaged Maven module (graphitron-docs) wired into the rewrite reactor via<module>../docs</module>;download-maven-pluginfetches@sikt/sds-coreand@sikt/sds-buttonfrom the npm registry tarball at pinned versions,maven-antrun-pluginflattenspackage/dist/index.cssintotarget/staging/css/sds-{core,button}.css,maven-resources-pluginstages authored/docs/*.adocplus/graphitron-rewrite/docs/*.adocunderarchitecture/,roadmap-tool render-adocemitsroadmap/{index,by-theme,changelog,plans/<slug>}.adoc, andasciidoctor-maven-plugin(in the default-ondocsprofile, opt-out via-P!docs) renders the merged tree totarget/generated-docs/withfailIf severity=WARNso missing xrefs, missing includes, and unresolved attributes fail the build. CI splits across three workflows:rewrite-build.ymlbuilds the rewrite reactor on PR and trunk push and adds trunk-onlydocs-build/docs-deployjobs (actions/upload-pages-artifact+actions/deploy-pages@v4,concurrency: { group: pages, cancel-in-progress: false });preview-docs.ymlbuilds the docs site on every PR touchingdocs/**,graphitron-rewrite/docs/**, orgraphitron-rewrite/roadmap/**and uploads the rendered tree as a workflow artifact; the legacymaven-build.ymlstays 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 403host_not_allowed), so the deployed Pages site has no runtime third-party dependency;sds-core’s `LICENSE.mdis copied intotarget/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/CNAMEfile shipped (custom domain set via Pages settings, the standard mechanism withactions/deploy-pages). Phase 5b (K8s deployment retire, GitLab CI pipeline retire,alf/graphitron-landingssidearchive) 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:69dropped theR68 scaffold-onlysecond sentence (the manual is shipped and populated),docs/manual/reference/directives/externalField.adoc:88anddocs/manual/how-to/computed-fields.adoc:152reframed thecomputed-field-with-reference.mdplan-slug references to feature-status notes,docs/manual/reference/directives/value.adoc:39softened "deferred to a follow-up roadmap item" to "UPSERT generation is deferred". Pre-existing drift inCLAUDE.md:74(Documentation site section) anddocs/README.adoc:6rewrote the stale.github/workflows/deploy-docs.ymlreference to point at thedocs-build/docs-deployjobs inrewrite-build.ymlpluspreview-docs.ymlfor PR previews. Build green:mvn -f graphitron-rewrite/pom.xml -pl :graphitron-docs -am packagerenders the merged tree under the WARN-fails policy on Java 25. -
R167 (
ee06817unify 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 throughRewriteContext.schemaFileExtensions(): Set<String>to three consumer sites that previously drifted independently.SchemaInputExpander.expandpost-filters scanner matches by extension;SchemaWatcher’s schema-mode constructor takes the `Set<String>instead of a hard-coded.graphqls;SchemaProblemDiagnostic.findOrphanSchemaFilesreads the configured set instead of its own hard-coded.graphql/.graphqlspredicate. The seven- and six-argRewriteContextoverloads default the new field toSet.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 aMojoExecutionException). The default matches the orphan scanner’s pre-R167 behaviour, so consumers with.graphqlfiles (Opptak’sregelverkMutations_exp.graphqlwas the concrete pain point) get thegraphitron:devwatcher firing on save and the<schemaInputs>glob-expansion picking the file up without configuring anything; teams reserving.graphqlfor client query documents opt in to the tighter<schemaFileExtensions><extension>.graphqls</extension></schemaFileExtensions>policy.SchemaWatcher’s single-string-suffix constructor stays for the `.classclasspath watcher (DevMojo.startClasspathWatcherstill passes".class"). Tests: unit-tierSchemaFileExtensionsNormaliserTest(6 cases: null returns default, missing-dot prepended, duplicates collapsed, whitespace trimmed, all-blank rejected, explicitly-empty rejected);SchemaInputExpanderTestaddsexpand_filtersFilesNotMatchingConfiguredExtensions,expand_dotGraphqlAccepted, andexpand_zeroMatchAfterExtensionFilter_throwsMojoExecutionException;SchemaWatcherTestaddsdispatch_triggersOnDotGraphql_whenConfigured,dispatch_ignoresUnconfiguredSuffix, andconstructor_emptySuffixSet_rejected;SchemaProblemDiagnosticTestaddsfindOrphanSchemaFiles_respectsConfiguredExtensions(tighten + loosen both checked).docs/manual/reference/mojo-configuration.adocdocuments the new parameter in the per-parameter reference table and updates thedevgoal description; the<schemaInput>row prose softens from.graphqlsfiles to "schema files (or globs that expand to schema files)". Out of scope (called out, not regressed): dropping the trailing/*.graphqlsoff<pattern>declarations so patterns describe directories only (purely additive on existing patterns); case folding (Linux is case-sensitive,.GraphQLsstays as authored); bundled directive files (directives.graphqlsis a classpath resource, not consumer-configured). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R158 (
f35644fadmit@service-backed producers for single-record DML carrier data fields;da25606pipeline + unit tests;129909cexecution-tier single-PK + ONE + empty + null-source cases;08acc84execution-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 typedXRecord/List<XRecord>. Closes the runtimeArrayList cannot be cast to org.jooq.Resultreproducer (OpprettRegelverksamlingPayload+opprettRegelverksamling @servicereturningList<RegelverksamlingRecord>): pre-R158 the data-field fetcher castenv.getSource()toResult<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 pathtoWrap.Record OR Wrap.TableRecord(target.recordClass()) + empty path; the load-bearing key renames in lockstep tosource-key.result-row-walk-target-aligned-empty-path(one@LoadBearingClassifierCheckonSourceKeyand one@DependsOnClassifierCheckonFetcherEmitterupdated, plus two javadoc references onChildField.java/SourceKey.java).FetcherEmitter.buildSingleRecordTableFetcherValuebecomes a sealed switch overSourceKey.Wrappermits: theWrap.Recordarm preserves the existing(Result<RecordN<...>>) env.getSource()/(RecordN<...>) env.getSource()casts andsource.getValues(<PK>)/source.value1()reads unchanged; the newWrap.TableRecordarm castsenv.getSource()to(List<XRecord>)(MANY) or(XRecord)(ONE) and reads PKs through the typedrecord.get(<XTable.<PK_FIELD>>)accessors (single-PK uses the column’s Java type as map key; composite-PK usesList.of(r.get(pk1), r.get(pk2), ...)for map-keying andDSL.row(pk1, pk2).in(source.stream().map(r -> DSL.row(...)).toList())for the response predicate);Wrap.Rowis the unreachable arm pinned by anIllegalStateException. Registration moves to per-producer helpers.GraphitronSchemaBuilder.registerCarrierDataField’s `DataElement.Tablearm hollows out; two new helpers inFieldBuilderare the only writers:registerDmlCarrierDataField(called from the non-DELETE DML kind classifier withWrap.Record) andregisterServiceCarrierDataField(called from theResolved.Resultarm of@serviceresolution withWrap.TableRecord(target.recordClass())). The@servicehelper does its own strictmethod.returnType().equals(expectedReturnType)check against the carrier walk’starget.recordClass(), colocated becauseServiceDirectiveResolver.computeExpectedServiceReturnTypereturnsnullfor carrier-payload return types by design. Orphan carriers (a carrier type returned only from a Query field with no producing mutation) now land with nofieldRegistryentry, structurally safe under graphql-java’s never-traverse-an-unproduced-field guarantee. R156’sregisterDeleteCarrierDataFieldTable arm passesnullforexpectedExistingClassin lockstep because the verbless walk no longer pre-registers;FieldRegistry.reclassify’s `expectedExistingClassparameter loosens to admitnull(admits both no-prior-entry and matching-prior-entry once the helper-side compare-then-write has confirmed wrap agreement). Producer-kind monomorphism: a newBuildContext.carrierProducerRegistrymap, keyed by(carrierType, dataFieldName)coords, records the first mutation that registers aSingleRecordTableFieldat each coord; the second producer’s helper reads it to enrich the rejection diagnostic when wrap shapes disagree (mixing a DML mutation and an@servicemutation on the same carrier type rejects at classify time, naming both producer mutations regardless of registration order). The rejection routes through the standardUnclassifiedField+Rejection.structural+validateUnclassifiedFieldpath; no parallel validator-mirror walk is needed. Load-bearing pins: producer@LoadBearingClassifierCheck(key = "carrier-data-field.single-producer-kind")onFieldBuilder.registerDmlCarrierDataFieldpairs with@DependsOnClassifierCheckonFetcherEmitter.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")onFieldBuilder.registerServiceCarrierDataFieldis a hygiene-rejection check (no consumer annotation owed; the cast safety it backstops is already pinned bysource-key.result-row-walk-target-aligned-empty-pathvia theWrap.TableRecord(target.recordClass())invariant). Tests: unit-tierSourceKeyTest(six cases pinning the loosenedReader.ResultRowWalkcompact-constructor invariant:Wrap.RecordandWrap.TableRecord(target.recordClass())admit; cross-tableWrap.TableRecord(other)rejects with target-aligned message; non-empty path rejects under either admitted wrap;Wrap.Rowrejects); pipeline-tierSingleRecordTableFieldServiceProducerPipelineTest(eight cases: ONE/single-PK, MANY/single-PK, and MANY/composite-PK FilmActor admission pins theWrap.TableRecord(target.recordClass())shape and registeredSourceKey.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-tierSingleRecordTableFieldServiceProducerExecutionTestagainst native PostgreSQL (five cases on SakilaFilmCarrierService/FilmActorCarrierServicefixtures: MANY-arm single-PK input-order preservation through the R141 PK-keyed-map walk; MANY-arm composite-PK input-pair-order preservation exercising the typedrow(pk1, pk2).in(...)predicate emission andList.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@servicereturns null → graphql-java does not traverse the carrier, payload rendersnullend-to-end);GraphitronSchemaBuilderTest’s `SINGLE_RECORD_CARRIER_DATA_FIELDrepurposes as_ORPHAN(Query-rooted carrier with no producing mutation: assert nofieldRegistryentry);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-JOINmigration (working code with its own audit surface; refactor of working code, not part of producer admission);Reader.ResultRowWalkconsumed outsideSingleRecordTableField(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);@serviceproducer withDataElement.Recorddata field (identity-passthrough permitSingleRecordIdentityFieldis producer-kind-irrelevant because the data field’s value IS the parent’s, regardless of producer). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R12 (carrier-walk LocalContext phases:
eb590efsplitErrorChannelinto sealedPayloadClass | LocalContext;c8731c9addTransportdiscriminator onChildField.ErrorsField;676ff72producer wiring forCarrierFieldRole.ErrorChannelRole;04799b8emitErrorRouter.dispatchToLocalContext;d7d1c55wire emit-time fork forErrorChannel.LocalContext;9cce63fselectErrorsField.Transportfrom parent’s resolved channel;a96766dregisterErrorsFieldon carrier-walk carriers +MappingsConstantNameDeduparms;093779cvalidator mirror for LocalContext errors-channel guard;f3ddcd4thread non-null sentinel through LocalContext catch path + pipeline / execute tests;93936d8anchor null-source guard sites to validator allow-list via audit annotations;4f1333frefresh 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’serrors: [SomeError!]field now routes a thrown exception into the typed payload instead of leaking the raw exception message through graphql-java’s defaultSimpleDataFetcherExceptionHandler. Foundational pieces landed first (sealedHandlertaxonomyExceptionHandler | SqlStateHandler | VendorCodeHandler | ValidationHandleronGraphitronType.ErrorType.Handlerwith parse-time lift and rules 1-6;ChildField.ErrorsFieldadmitting where the fivePolymorphicReturnTyperejection arms used to fire; theErrorChannelcarrier withmappedErrorTypes,payloadClass,errorsSlot,defaultedSlots,mappingsConstantName; classify-timeMappingsConstantNameDedupwith 8-hex SHA-256 collision suffix; channel-level rule 7 multi-VALIDATION + rule 8 duplicate-criteria checks;ErrorRouter.dispatch/redact/Mappingtaxonomy emitted at<outputPackage>.schema.ErrorRouterwithErrorMappingsconstants alongside;(List<String>, String)-ctor classifier check viaClass.forName; per-@error-union/interfaceTypeResolverregistration and per-@error-typepath/messagefield DataFetchers inGraphitronSchemaClassGenerator; source-direct dispatch with no developer@errorJava backing class;Optional<ErrorChannel>slot on everyWithErrorChannelpermit; per-fetcher try/catch wrapper +.exceptionallyasync tail routing the catch arm throughErrorRouter.dispatchorErrorRouter.redact; DML payload assembly + dispatch;ResultAssemblyfor service-side payload assembly; rule 6 relaxation + per-(channel, @error type, handler)source-class accessor reflection check via R88’sClassAccessorResolver;extensions.constraintfield population onConstraintViolations.toGraphQLError; child@service/@tableMethoderrorChannellift;@service/@tableMethoddeclared-exception channel-coverage check). The In Review pass shipped the carrier-walk LocalContext story (the R161 enabler):ErrorChannelis now a sealedPayloadClass | LocalContextinterface with the channel-agnosticmappedErrorTypes()/mappingsConstantName()accessors; theBuildContext.classifyCarrierFieldproducer admits errors-shaped wrappers asCarrierFieldRole.ErrorChannelRolewith aLocalContextbinding ahead ofDataChannelresolution;ChildField.ErrorsFieldgrows aTransport transport()component (PayloadAccessor | LocalContext) selected at classify time from the parent’s resolved channel;TypeFetcherGenerator.catchArmandasyncWrapTailswitch exhaustively on the sealed root and emitErrorRouter.dispatchToLocalContext(...)for the LocalContext arm. The runtime fix threads a typedP sentinelthroughdispatchToLocalContext: graphql-java’scompleteValueForObjectshort-circuits children on a null parent, so the catch arm now packsdata(sentinel).localContext(List.of(t)).build()wheresentinel = DSL.using(SQLDialect.DEFAULT).newRecord(<pk>)(single) /newResult(<pk>)(bulk); the data field’s null-source guard renders the SDL response asdata: nullwhile the errors field reads viaenv.getLocalContext(). ThecatchArmhelper 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")onBuildContext.classifyCarrierFieldpairs with@DependsOnClassifierCheckonTypeFetcherGenerator.dispatchToLocalContextCatchArm, theTransport.LocalContextarm ofFetcherEmitter.dataFetcherValue,FieldBuilder.transportForParent, the validator mirrorGraphitronSchemaValidator.validateLocalContextErrorsFieldGuards, and the four per-variant emitter sites (buildSingleRecordTableFetcherValueRecordWrap/...TableRecordWrap,buildSingleRecordIdFromReturningFetcherValue,buildSingleRecordTableFromReturningFetcherValue,buildSingleRecordIdentityFetcherValue) that anchor eachif (source == null) return null;guard to the audit harness. The validator mirror rejects schemas whoseErrorsFieldcarriesTransport.LocalContextbut whose sibling data-channel field is outside theLOCAL_CONTEXT_GUARDED_DATA_CHANNEL_VARIANTSallow-list, turning a future widening that admits a non-guarded variant into a build-timeRejection.AuthorError.Structuralrather than a request-time NPE. Tests: unit-tierErrorRouterClassGeneratorTest(13 cases pinningdispatchToLocalContext(thrown, mappings, env, sentinel)signature + body packs sentinel intodata()); pipeline-tierSingleRecordCarrierPipelineTest(34 cases including the three R12 + R161 integration tests: single-inputMutationDmlRecordFieldand bulk-inputMutationBulkDmlRecordFieldeach classify witherrorChannel = Optional.of(LocalContext)and a siblingErrorsFieldwithTransport.LocalContext; emit pin forSQLDialect.DEFAULT/newRecordsentinel construction andenv.getLocalContext()reading); pipeline-tierErrorChannelClassificationTest(21 cases including carrier-walk LocalContext admission + rule 7 multi-VALIDATION rejection through the unified walk); validator-tierLocalContextErrorsFieldValidationTest(3 cases: guarded sibling passes, unguarded sibling rejects with allow-list diagnostic,PayloadAccessorErrorsField with unguarded sibling untouched); audit-tierLoadBearingGuaranteeAuditTestpicks up the producer + all consumers via the annotation scan; execute-tierGraphQLQueryTestadds Sakila SDL fixturesFilmCreateLocalContextPayload+FilmCreateConstraintViolation@errortype (handlerGENERICagainstorg.jooq.exception.IntegrityConstraintViolationException) with two end-to-end paths: validlanguageId=1round-trips with{film: {...}, errors: null};languageId=99999trips PostgreSQL FK 23503, routes throughdispatchToLocalContext, 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@serviceResultAssembly.Assemblyarm (split to R169, not blocked); execute-tier coverage for the JakartaValidationHandlerchannel (split to R170, blocked on R94emit-input-records); subscription error paths; batch-loader per-key error handling; federation entity-resolver errors; instrumentation hooks; transaction rollback semantics; consumer-facingExceptionHandlingBuilderanalogue (auto-wiring is the goal, no top-level handler to install); customExecutionStrategyfor non-error reasons. Behaviour shifts vs legacy (documented in user-facing migration table):IllegalArgumentExceptionmessages are no longer automatically exposed to clients (schemas relying on the legacy auto-leak must declare{handler: GENERIC, className: "java.lang.IllegalArgumentException"});DATABASEhandlers now match anySQLExceptionin the cause chain, not only those wrapped in Spring’sDataAccessException(non-Spring apps no longer needspring-jdbcfor database error mapping). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R161 (
d0f676cretireDmlReturnExpression.Payloadand unify@record-returning DML on the carrier-walk path;8a9a707lift theNoBacking/ClassBackedfork intoSingleRecordCarrierResolution.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 throughMutationDmlRecordField/MutationBulkDmlRecordFielduniformly;Mutation*TableFieldpermits are now guaranteed never to carry a@recordreturn, enforced structurally rather than via classifier-acceptance shape. TheDmlReturnExpressionsealed type collapses to four arms (EncodedSingle/EncodedList/ProjectedSingle/ProjectedList); thePayloadAssembly/RowSlotmodel types, the reflection-based resolver (resolveDmlPayloadAssembly,buildDmlPayloadAssemblyBeanArm,DmlPayloadAssemblyResult,NO_ASSEMBLY), and the emit layer (emitPayload,emitPayloadCtor,emitPayloadSetters) all retire.MutationInputResolver.validateReturnTypedrops thefqClassName == nullguard on theResultReturnTypearm so the carrier-walk probe runs unconditionally ; one probe over the SDL shape, not two probes composing. The follow-up commit lifts the consumer-sideparentType instanceof PojoResultType.NoBackingre-narrowing atGraphitronSchemaBuilder:227into the model as a sealedOkinterface withNoBacking/ClassBackedrecord sub-arms (BuildContext.tryResolveSingleRecordCarriertags the outcome viatarget instanceof ResultType && !(NoBacking)), per Generation-thinking: type-level classification short-circuits to carrier-walk registration only onNoBacking;ClassBackedfalls 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 `@tabletype" on the fourMutation*TableFieldrows) and gains two new rows forMutationDmlRecordField/MutationBulkDmlRecordField; the.returningResult(pkCols)design decision is captured onbuildMutationDmlRecordFetcherandbuildMutationBulkDmlRecordFetcher(PK-only RETURNING keeps the write transaction minimal; data-field projection runs in a separate read-only follow-up SELECT outside the transaction).LoadBearingClassifierCheckdescriptions onresolvePayloadConstructionShapeandbuildDmlFieldare trimmed to reflect the post-R161 consumer set (DML-rowemitPayloadremoved; four-armDmlReturnExpression). Tests: pipeline-tierDML_RECORD_PAYLOAD_RETURN_HAPPYandDML_RECORD_PAYLOAD_ROW_ONLY_HAPPYre-target as carrier-walk admission tests (DML_RECORD_CARRIER_WITH_ERRORS_HAPPY/DML_RECORD_CARRIER_ROW_ONLY_HAPPY) exercising theJavaRecordTypewrapper arm admitted by the R161 widening;DML_RECORD_PAYLOAD_NO_ROW_SLOT_REJECTEDrepurposes asDML_RECORD_CARRIER_NO_DATA_CHANNEL_REJECTED(the carrier walk rejects the SDL shape for missing aDataChannelfield rather than reflecting on the developer’s class);DML_RECORD_PAYLOAD_LIST_REJECTEDkeeps its name with the rejection diagnostic now coming fromvalidateReturnTypeinstead ofresolveDmlPayloadAssembly.FetcherPipelineTest’s `dmlMutation_setterShapePayload_emitsSetterFactory,dmlDeleteField_recordPayloadReturn_successArmConstructsPayloadAndCatchArmDispatches,dmlDeleteField_recordPayloadReturnNoErrorsField_successArmConstructsPayloadCatchArmRedacts, and the unusedSetterShapeDeleteFilmPayloadfixture all delete (the bodies they asserted on no longer exist). After migration: zero references toDmlReturnExpression.Payload,PayloadAssembly,RowSlot,emitPayload*, orresolveDmlPayloadAssemblyanywhere in the codebase. Out of scope (called out, not regressed): execution-tier coverage for the fourResultTypeclassName-carrying arms (sakila’sFilmPayloadalready pins the carrier-walk emit shape viaNoBacking, and post-R161 every wrapper state runs the same emitter code, so a bespoke fixture would assert structural rather than behavioral coverage); consolidatingMutationFieldpermits under verb-on-permit-identity (MutationInsertResultField/ etc., tracked at R162). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R39 (
f8fc604wip validator + tests;d0cf6b5validator ship + carrier permit lift;2600955lift PK-derived orderBy toOrderingOwnedByProducersealed marker): Cross-cutting validator rejectsSqlGeneratingField+FieldWrapper.List+OrderBySpec.Noneat build time, closing the silent-non-determinism gap on list fields targeting no-PK tables (OrderByResolver.resolveDefaultOrderSpecfalls back toOrderBySpec.Nonewhen no@defaultOrder/@orderByis present and the target has no PK, which generators faithfully emit asList.of(); noORDER BY; producing visibly different row order each run). Three checks now cover three disjoint shapes:validatePaginationRequiresOrdering(paginated, including connections),validateSplitTableFieldconnection branch (@splitQuery connections),validateListRequiresOrdering(plain[T]list wrapper). Gated onFieldWrapper.Listnotwrapper().isList()so the three messages stay non-overlapping; the sealedFieldWrapperpermit list (Single/List/Connection) is the typed gate. Marker carrier: a new sealedOrderingOwnedByProducer(model package) permitsChildField.SingleRecordTableFieldandChildField.ServiceTableField; the validator excludes marker-bearing permits by type. The carrier permit’s structurally-emptyorderBy() = Nonestays ; 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 `@servicemethod forServiceTableField), not by the field’s ownorderBy()component. An earlier shape derived a PK-fixedOrderBySpec.FixedfromsourceKey.columns()insideSingleRecordTableField.orderBy()to side-step the validator; principles-architect flagged this as duplicatingOrderByResolver.resolveDefaultOrderSpec’s no-directive branch and coupling the validator’s correctness to a `FieldBuilderclassifier 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/buildBaseReturnExprandInlineTableFieldEmitterdefensively handleNone/emptyFixedforSingleandConnection-without-pagination shapes the validator does not gate), so no@LoadBearingClassifierCheck/@DependsOnClassifierCheckpair is owed. Tests: unit-tierListRequiresOrderingValidationTest(5 cases pinning Query-rooted + child-position dispatch path × list/single × ordered/unordered); pipeline-tierValidateListRequiresOrderingPipelineTest(reject + admit through SDL → classified model →GraphitronSchemaon the Sakila no-PKfilm_listfixture, asserting the contract’d error message); five pre-existing tests ({Lookup,QueryLookup,RecordTable,RecordLookupTable}FieldValidationTest) updated to use PK-fixed orderBy where they incidentally usedOrderBySpec.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); changingOrderByResolverto refuseOrderBySpec.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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R165 (
28cea70gate empty fetcher-registration bodies at construction):FetcherRegistrationsEmitter.emitwas producing empty-body entries for unreferenced payload-shaped types (the field-report reproducer was an unreferencedSlettRegelverksamlingPayload { regelverksamlingId: [ID!] @nodeId }), which surfaced as ajavacerror in consumer projects:GraphitronSchema.build()emitted<Name>Type.registerFetchers(codeRegistry)for every key in the bodies map, whileObjectTypeGeneratorskipped the method emission when the body was empty. Fix gates emptiness at the construction site rather than via a post-pass scrub:typeBodyandnestedBodyreturnOptional<CodeBlock>(empty when the classified-field list is empty), and the twoputcall sites inemituse.ifPresent(body -> result.put(name, body)). The deadfields.isEmpty()short-circuit insidebuildBodyis 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 keyfetcher-registrations.no-empty-bodiespins the producer-side guarantee:@LoadBearingClassifierCheckonFetcherRegistrationsEmitter.emit, matching@DependsOnClassifierCheckonGraphitronSchemaClassGenerator.generate’s keyset iteration; `LoadBearingGuaranteeAuditTestcatches future drift as an orphaned-consumer audit failure rather than a downstreamjavacerror. Tests: unit-tierFetcherRegistrationsEmitterTest(post-condition thatemit’s returned map has no empty `CodeBlockvalues, plus key-absence on the bug-reproducing payload fixture, across single-record carrier and connection/edge code paths); pipeline-tierFetcherRegistrationsPipelineTestasserts the bi-directional set-equality invariant between<Name>Type.registerFetchers(codeRegistry)call sites in the emittedGraphitronSchema.build()body and the type names whoseObjectTypeGeneratorTypeSpecdeclares aregisterFetchersmethod, 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 viaadditionalTypes); strengtheningemit’s return type to a `FetcherBodiesrecord orMap<String, NonEmpty<CodeBlock>>carrier (R166 floats the broaderGraphQLSchemaVisitor-driven emission rework where this would land). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon 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
@servicepayload construction shape ; public no-arg constructor + per-SDL-field Java-bean setters ; alongside today’s canonical all-fields constructor. New sealedPayloadConstructionShape { AllFieldsCtor | MutableBean }carries the contract;MutableBean.bindingsholds oneSetterBinding(sdlFieldName, setter, acceptsOptional)per SDL field in declaration order. The three carrier slot types lift in lockstep:ErrorsSlot,ResultSlot,RowSloteach become sealed withCtorParameterIndex(int)+SetterMethod(boundSetter, List<NonBoundSetter>)permits (kept as three sibling hierarchies, not folded onto one broadSlotinterface, soErrorChannel/ResultAssembly/PayloadAssemblykeep role-specific access withoutinstanceofwidening). NewNonBoundSetter(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 sealedPayloadConstructionShapeResult { Resolved(shape) | Reject(reason) }; predicates run in order withAllFieldsCtorfirst (canonical-over-bridge precedence: records always present the all-fields ctor; the setter shape is a legacy bridge fromgraphitron-codegen-parent); a class supporting both shapes resolves toAllFieldsCtor; 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 onPayloadConstructionShapevia exhaustive sealed switch; the bean arm emitsvar p = new Payload(); p.setBound(...); p.setOther(<default>); ...; return p;instead ofnew Payload(...). Load-bearing pins: producer-side@LoadBearingClassifierCheckannotations onresolvePayloadConstructionShapefor the keyspayload-construction.shape-resolved(carrier-arm-totality) andpayload-construction.setter-name-matches-sdl-field(setter.getName() is callable into the generated source); consumer-side@DependsOnClassifierCheckon the three emit-site forks plus the carrier helpers. Tests: unit-tierPayloadConstructionShapeTest(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 namingxRating → setXRating); pipeline-tierFetcherPipelineTestfour R154 cases (serviceMutation_setterShapePayload_emitsSetterFactory,_allFieldsCtorPayload_emitsCtorFactory_unchangedregression,_bothShapesPresent_prefersCtorFactory,dmlMutation_setterShapePayload_emitsSetterFactory); compilation-tiergraphitron-sakila-serviceaddsSetterShapeFilmReviewPayload(no-arg ctor +setReviewId/setErrors) plusFilmReviewService.submitSetterShapereturning that type; sakila-example schema addssubmitSetterShapeFilmReviewmutation; execution-tierGraphQLQueryTesttwo end-to-end cases against real PostgreSQL (submitSetterShapeFilmReview_validInput_returnsHappyPathPayloadand_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 separateBuilderPatternpermit onPayloadConstructionShapeif a real schema surfaces it; replacingAllFieldsCtorwithMutableBean(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.acceptsOptionalis captured by the predicate but not consumed by any emit site (anOptional<T>setter receivesnullrather thanOptional.empty()for default-slot inserts); the spec’s "parameter-type mismatch rejection" unit case is unimplemented (tryMutableBeanaccepts any single-arg setter on name match alone, the legacy convention’s de-facto behaviour). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R156 (
ba4697fPhase A model:DataElement.Id,PerFieldOutcome,PkResolution, two newChildFieldsiblings;cbe4634Phase B verb-aware carrier walk + DELETE projection;61ce2c8Phase C/DFieldBuilderrewire +MutationFieldDELETE admission lift +FieldRegistry.reclassify;8d88bb5Phase E/F per-field emitters + DELETE DML chain emission;fe676bfPhase G partial L1/L3;a869716Phase G L5 sakila fixtures;160f102Phase G L6 execution-tier coverage;e08c439Phase H user docs;424bc42doc sweep;b5209a9In Review rework ;PkResolutionEmitterReachabilityTest,MutationDmlNodeIdClassificationTestadmission cells,MutationDeletePayloadCarrierCaserejection 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’serrors:channel). NewDataElement.Id(name, wrapper)arm admits anID/[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 existingDataElement.Tablearm narrows on DELETE: a new builder-internalPerFieldOutcome(five arms:PkRead,NonPkNullable,NonPkNonNullable,ServiceField,UnsupportedField) classifies every field on the element SDL type, andBuildContext.classifyDeleteTableProjectioneither rejects (any non-PK-non-null /@service/ FK-traversing / unsupported leaf, with a diagnostic naming the offending field) or projects to the narrow model-facingPkResolution(two arms) carried on a newChildField.SingleRecordTableFieldFromReturningpermit. The companionChildField.SingleRecordIdFieldFromReturningcarries theCallSiteCompaction.NodeIdEncodeKeysfor theIdarm; both are siblings of the existingSingleRecordTableFieldand load-bearing for distinct invariants (no follow-up SELECT after DELETE, the row is gone).BuildContextgains a verb-awaretryResolveSingleRecordCarrier(typeName, DmlKind)overload that delegates to the verbless walk and layers DELETE-admissibility on top, so the unconditional DELETE-rejection atFieldBuilder.java:2960-2965disappears entirely;MutationDmlRecordField/MutationBulkDmlRecordFieldcompact constructors lift the DELETE rejection symmetrically.FieldRegistry.reclassifyis the named exception that lets the DELETE carrier path replace the verbless walk’sSingleRecordTableFieldregistration (which assumed follow-up SELECT) with the DELETE-specific sibling.FetcherEmitteradds two methods:buildSingleRecordIdFromReturningFetcherValuereads PK column(s) off the sourceRecordand runs them through the encoder;buildSingleRecordTableFromReturningFetcherValuesynthesizes a PK-onlyRecordviaTables.<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")onclassifyDeleteTableProjectionplus matching@DependsOnClassifierCheckon the table-arm emitter. Tests: unit-tierDataElementIdInvariantTest(6 cases pinning compact-constructor wrapper invariants, singleton ID/ID! and [ID!]/[ID!]! admit, list-of-nullable and Connection wrappers reject); unit-tierPkResolutionEmitterReachabilityTest(4 cases: reflective scan ofPkResolutionarms againstFetcherEmitter’s `HANDLED_BY_EMITTERallowlist,PerFieldOutcomerejection arms exist and do NOT leak intoPkResolution, record-component symmetry across the two sealed roots,@LoadBearingClassifierCheckpin reflectively confirmed onclassifyDeleteTableProjection); pipeline-tierMutationDmlNodeIdClassificationTestgains six R156 rows (single/bulk × implicit/explicit@nodeIdadmission cells overnodeidfixture’s composite-PK `Barand single-PKBaz, plus wrong-encoder-table and no-@node-backed-input-table rejection paths); pipeline-tierMutationDeletePayloadCarrierCaseparameterised inGraphitronSchemaBuilderTestcovers the admission/rejection matrix (nullable non-PK admits withPkResolution.NonPkNullableprojection; non-null non-PK rejects naming the field and pointing atDataElement.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-tiergraphitron-sakila-exampleaddsDeletedFilmsIdPayload,DeletedFilmsTablePayload,DeletedFilmInfotypes +deleteFilmsIdCarrier/deleteFilmsTableCarriermutations; execution-tierDmlBulkMutationsExecutionTestadds 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 atdocs/manual/reference/directives/mutation.adocrather thandocs/manual/reference/mutations.adocbecause the existing layout puts directive references underdirectives/. 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;ErrorChannelRolepermit 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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R159 (
6aababeadmit + type-match + LSP arms;8671b5bthread sourceSigil into DataChannel, route LSP through siteContext, drop back-compat constructors): root-value sigil$sourceon@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). NewFieldSourceSigilutility owns the sealedFieldNameRef = BareName | UpstreamRoot,ParseResult = Absent | Ok | UnknownSigil, andSiteContext = CarrierDataField | Other; the three canonical messages (unknown sigil, not-defined-here, type mismatch) live on the utility so classifierHardReject, LSPDiagnostics, and LSPFieldCompletionsroute through one source.BuildContext.classifyCarrierFieldinterposesparseArgFieldNameRefbefore the forbidden-directive loop:UpstreamRootlifts@fieldoff the forbidden list for the iteration and threadssourceSigil = trueinto the emittedCarrierFieldRole.DataChannel(fieldName, element, sourceSigil);UnknownSigilHardRejects before the forbidden-directive loop fires (so the author sees "Unknown sigil" rather than "forbidden directive"). The type-match check runs atFieldBuilder.classifyMutationField’s `@service Resolved.Resultarm (the colocation principle is preserved by the sharedsourceSigilTypeMatchescallable, with the bit-read replacing the SDL re-parse). LSP plumbing:CatalogBuilder.projectCarrierDataFieldswalksGraphitronSchema.fields()forChildField.SingleRecord*permits to projectMap<String, String> carrierDataFieldByTypeontoLspSchemaSnapshot.Built;Built.siteContext(typeName, fieldName) -> FieldSourceSigil.SiteContextis the one entry point consumers use, so broadening admit in a future item flips a single sealed return-value.FieldCompletionsadmits$sourceat carrier-data-field sites and stays silent everywhere else (including snapshot-uncertainty: no entry in the carrier projection → no suggestion).Diagnostics.validateFieldMemberemitssourceSigilNotDefinedHereMessageat non-carrier sites whose parent’sTypeBackingShapeis known; snapshot-uncertainty stays silent (defers to the build).LspSchemaSnapshot.Built.Current/Built.Previouscollapse to single canonical three-arg constructors; the two-arg back-compat overloads were a shim with no external producer to protect. Tests: pipeline-tierFieldSourceSigilPipelineTestcovers admit, model-shape regression (with/without@fieldbyte-identical), type-mismatch reject, unknown-sigil reject (parse-time arm fires before forbidden-directiveHardReject), bare-name regression, non-carrier-site regression (today’s accessor-mismatch unchanged) ; each rejection case also asserts validator-surfaceValidationReport.errors()via the same fixture; LSP-tierFieldCompletionsTest+DiagnosticsTesteach gain three R159 cases (admitted / non-carrier / snapshot-uncertainty). Deviation from spec (called out in spec body): type-match site isFieldBuilder.classifyMutationField, notclassifyCarrierField, because the producer’sMethodRefis bound at consumer-site classification not at the carrier walk;FieldSourceSigil.sourceSigilTypeMatchesuses exact equality for bothDataElement.TableandDataElement.Recordtoday (spec called for assignability on the@recordarm; 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 flipssourceSigilDefinedAt);$errors/$context/ other sigils; dotted paths in@field(name:); multi-step path-expression grammar; DML-producer carrier walk migration to a$sourcemodel (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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R139 (dev-pipeline to LSP schema-snapshot side-channel; first client unknown-directive validator): the bundled
directives.graphqlswas the LSP’s entire view of "what directives exist", so any user-authored directive (@key(fields:),@requiresAuthentication,@auth(role:), etc.) drew aDiagnosticswarning per use as if it were a typo. Fix: a secondvolatileref onWorkspace(alongsidecatalog) carrying a projection of the parsed user schema, populated from the same parse the catalog already runs. New sealedLspSchemaSnapshot(Unavailable | Built.{Current,Previous}) over two orthogonal axes ; availability (built yet?) and freshness (latest successful parse?) ; lifts theDiagnostics.validateClassName-style "pre-build silence" gate from a single arm into a typed sub-hierarchy.BuiltcarriesList<DirectiveShape>with siblingInputValueShape+ sealedTypeShape(Named | List) so phase-2 arg-validation consumers discriminate list-vs-named without re-parsing rendered SDL. Producer isCatalogBuilder.buildSnapshot(TypeDefinitionRegistry)(returnsBuilt.Currentonly; failures throw upstream inGraphQLRewriteGenerator), pinned with@LoadBearingClassifierCheckon the keyssnapshot-built-implies-clean-parseandsnapshot-directive-roundtrip-faithful. Atomic-pair swap path isWorkspace.setBuildOutput(...)(absorbing the spec’s separately draftedsetCatalogAndSnapshotandsetCatalog-on-classpath setters, since the validator-report side-channel that landed on top of R139 wants the same atomic swap discipline); failure path isWorkspace.demoteSnapshot()which transitionsBuilt.Current → Built.Previousand is no-op onUnavailable/Previous. Resolution goes through sealedDirectiveResolution.resolve(LspVocabulary, LspSchemaSnapshot, String)returningBundled | 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 aWorkspace. First client: the unknown-directive arm inDiagnostics.computeswitches exhaustively on the snapshot variant for the freshness-aware silence policy ; warns only underBuilt.Current + Unknown, silencesUnavailable(pre-build),Built.Previous(stale after parse failure), and anyUser/Bundledresolution.SPEC_BUILTIN_DIRECTIVES(skip,include,deprecated,specifiedBy,oneOf) keeps its short-circuit because graphql-java ships them implicitly.DevMojo.regenerate/rebuildCatalogboth callsetBuildOutput(...)on success anddemoteSnapshot()+markAllForRecalculation()on the parse-failure catch. Tests: unit-tierLspSchemaSnapshotTest(case-sensitive lookup across bothCurrent/Previous, unmodifiable defensive copy at construction) andCatalogBuilderSnapshotTest(directive round-trip, list/non-null sealed projection, no producer-side bundled-name filter, description round-trip); pipeline-tierDiagnosticsTestgrowsunknownDirectiveSilencedByUnavailableSnapshot,unknownDirectiveSilencedByStaleSnapshot,userDeclaredDirectiveSilencedBySnapshot,userDeclaredDirectiveShadowedByBundledStillValidates, plus the existingunknownDirectiveProducesWarningupdated to pass an explicitBuilt.Current(List.of(), Map.of()); compilation-tier sakila fixture declaresdirective @auth(role: String!) on FIELD_DEFINITIONand applies it onQuery.customersas the input-contract regression guard. Out of scope (called out, not regressed): hover / arg-completion / arg validation against user directives (later items; phase-2 armvalidateUnknownArgsAgainstSnapshot/validateRequiredArgsAgainstSnapshotrides on the same plumbing); adeclaredTypeNamesset onBuilt(R157 widened the permits withtypesByNamefor the analogous record/POJO use case); wideningLoadBearingGuaranteeAuditTestacross the graphitron / graphitron-lsp module boundary (consumer-side@DependsOnClassifierCheckmarkers onDiagnostics.computeandWorkspace.resolveDirectiveare find-usages-only by design); a shadow-warning for user directives that redeclare bundled names; server-mode LSP without the dev mojo. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R157 (
6c33331implement LSP@field(name:)coverage for@record-bound types;6b9ea86sealed-splitJooqRecordBacking, 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@tabledirective 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 sealedTypeBackingShape(RecordBacking | PojoBacking | JooqRecordBacking{WithTable|Standalone} | TableBacking | NoBacking{Root|UnbackedResult|UnclassifiedInterface}) projects everyGraphitronTypepermit to the LSP-visible backing shape; the projector lives inCatalogBuilder.projectTypeas an exhaustive sealed switch so a futureGraphitronTypevariant trips a compile error at the projection site.LspSchemaSnapshot.Built.{Current,Previous}broaden to carryMap<String, TypeBackingShape> typesByNamealongside the R139directiveslist;GraphQLRewriteGenerator.buildOutputwires the three-argbuildSnapshot(registry, schema, catalog)form.ClasspathScannerreads the JVMRecordattribute viajava.lang.classfile.attribute.RecordAttributesoCompletionData.ExternalReferencecarriesList<RecordComponent>per scanned class; the projector consumes these forRecordBackingand the bean-accessor filter (get<X>/is<X>no-arg public method) lives inCatalogBuilder.beanAccessorSlotforPojoBacking. The three consumer sites pattern-dispatch on the sealed permit (Diagnostics.validateCatalogColumnrenamed tovalidateFieldMember, since it dispatches across four backing shapes plusNoBackingnow);TypeContext.tableNameOfis 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 onCatalogBuilder.buildSnapshotpins the assumption the three LSP-side@DependsOnClassifierCheckconsumers make aboutJavaRecord{Input,}Typebacking real Java record classes; the audit-test scope isgraphitron-module-only by design (the LSP-side consumers wear the annotation for find-usages navigation and reviewer-signal purposes perrewrite-design-principles.adoc § "Classifier guarantees shape emitter assumptions"’s producer-without-consumer allowance). Tests: primary-tier `R157PipelineTestparses a realistic.graphqls, runsGraphitronSchemaBuilderfor real, scans the LSP module’starget/test-classesfor fixture classes (R157FilmRecord,R157FilmPojo), builds the full snapshot throughCatalogBuilder, and drivesFieldCompletions/Diagnosticsend-to-end; unit-tierCatalogBuilderSnapshotTestadds per-variant projection cases (TableType, TableInterfaceType, JavaRecordType, PojoResultType.Backed, JooqTableRecordType, JooqRecordType-standalone, RootType, plain InterfaceType, PojoResultType.NoBacking);ClasspathScannerTestcovers the Record-attribute read plus the plain-class empty case;FieldCompletionsTest,DiagnosticsTest,HoversTestadopt the snapshot-keyed dispatch and add positive cases perTypeBackingShapearm (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 existingTypeContext.tableNameOfpath); union types as@fieldparents (meaningless directly; flows toNoBacking.UnbackedResult); migrating@nodeId(typeName:)’s metadata projection onto `typesByName(R152 owns the table-of-other-type scoping bug); per-component nullability / Jakarta-constraint surfacing onMemberSlot(R12-adjacent). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R153 (
555fa0f+32c7ebe): Attach explicitTextEditrange to every LSPCompletionItem. Hoists the cursor walk + replace-range computation intoLspVocabulary.locateAt(returnsOptional<CursorLocation>carrying the schema coordinate plus the tree-sitter leaf node) andCompletionContext(carries the LSPRangederived by slicing the leaf:string_valuestrips one or three bytes per side, discriminated by content;enum_value/ barenameuse the full span);coordinateAtcollapses to a thin wrapper preserving theHoverscaller. All six string-value providers (ClassName,Method,Table,Field,Reference,ScalarType) plusNodeTypeCompletionsandArgNameCompletionsshipsetTextEdit, so eglot’s graphql-mode syntax table (which excludes.as a symbol constituent) no longer concatenates the prefix with the candidate on dotted FQNs likecom.example.FilmServ|becomingcom.example.com.example.FilmService. Empty literals and block strings flow through the same slicing rule (""and""""""collapse to zero-width at the inner cursor);ArgNameCompletionsalso 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) andCompletionTextEditTest(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):filterTextfor partial-match scoring andinsertTextsnippet 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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R155 (
0bd77deboundary box + invariant pin;fc55fa6emitter-tier pin):graphitron:devblew up withIllegalArgumentException: couldn't make a guess for inton consumer schemas whose input beans carried a Java-primitive field (recordint n, JavaBeanvoid setActive(boolean), …) becausejava.lang.reflect.Type.getTypeName()returns the unboxed primitive literal and that string flowed unchanged throughFieldBinding.javaElementTypeNameinto the twoClassName.bestGuesssites inInputBeanInstantiationEmitter(fieldLocalType,directExpr). Fix: normalise at the resolver boundary. NewInputBeanResolver.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;peelJavaListSetcalls it on the scalar return soFieldBinding.javaElementTypeNamebecomes invariantly a real class name, never a Java primitive literal. The list branches deliberately don’t box: Java disallowsList<int>, so the generic argument is always already a reference type. Javadoc on the helper and a sentence onFieldBinding.javaElementTypeNamepin the contract as prose; no@LoadBearingClassifierCheckannotations owed (this is representation-normalisation at the resolver, not a new classifier branch with downstream shape obligations). Tests: unit-tierInputBeanResolverBoxPrimitiveTestpins the full 8-arm primitive→wrapper mapping plus class-name pass-through plusint[]fallthrough; pipeline-tierGraphitronSchemaBuilderTestgains two cases (SERVICE_MUTATION_FIELD_INPUT_BEAN_PRIMITIVE_RECORD,SERVICE_MUTATION_FIELD_INPUT_JAVABEAN_PRIMITIVE_BOOLEAN) assertingFieldBinding.javaElementTypeName == "java.lang.Integer"/"java.lang.Boolean"on the record-component and JavaBean-setter paths respectively;TypeFetcherGeneratorTestgains two mirror cases pinning thatInputBeanInstantiationEmitter.buildSingularHelperno longer throws on a boxed primitiveFieldBindingand emits the wrapper-typed local and cast. Out of scope (called out, not regressed): generalisingClassName.bestGuessingraphitron-javapoetto accept primitives (javapoet treats primitives viaTypeName.INT, a wider refactor of no immediate value); tightening theString-typed representation ofFieldBinding.javaElementTypeName/EnumValueOf.enumClassNameto a typedClassNameor sealedJavaTypeRefso the "real class name, never a primitive literal" invariant is a type fact rather than prose (separate Backlog item). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R144 (
db40644ship cardinality safety default;a142f78cover@value+@conditionmutual-exclusion rejection;11bd6eedrop unreachableadmissibleCount == 0branches): Flip the polarity of mutation-input safety. Every input field on a DELETE / UPDATE@tableinput 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.@valueon input fields marks UPDATE assignment columns; rejected on DELETE / INSERT / UPSERT and mutually exclusive with@conditionon the same field.@lookupKey on INPUT_FIELD_DEFINITIONis retired with a migration diagnostic surfaced at both per-field and per-arg classification sites (theARGUMENT_DEFINITIONuse for Query-sideLookupTableField/SplitLookupTableFieldis untouched). UPSERT is refused upstream atMutationInputResolverwith aRejection.deferredkeyed to R145 (mutation-cardinality-safety-upsert); R141’s compact-constructor UPSERT rejection onMutationBulkDmlRecordFieldbecomes a redundant type-system backstop during the R144-shipped-but-R145-not-yet window. Carrier change:TableInputArg.ofnow takesDmlKind kindand the@value-marked field-name set; partitionslookupKeyFields/setFieldsper verb (UPDATE: complement vs intersection on the@valueset; DELETE / INSERT:setFieldsempty by classifier guarantee).EnumMappingResolver.buildLookupBindingsdrops theDIR_LOOKUP_KEYgate and walks every admissible input field minus a caller-supplied exclude set (the UPDATE@valuenames). Audit producers: two new@LoadBearingClassifierCheckkeys onMutationInputResolver.resolveInput(mutation-input.where-columns-cover-pk,mutation-input.update-set-fields-equal-value-marked) with@DependsOnClassifierCheckconsumers on the eleventia.setFields()walk sites inTypeFetcherGenerator’s UPDATE arms and on the `MutationBulkDmlRecordFieldconstruction site (so any future refactor that branches the bulk path aroundresolveInputsurfaces as an orphaned consumer inLoadBearingGuaranteeAuditTest). Migration: sakila example schema migrated (upsertFilm/upsertFilms/upsertFilmPayloadretired;FilmUpdateInput.title/.descriptioncarry@value; newFilmReleaseYearDeleteInput+deleteFilmsByReleaseYearmultiRow fixture); classifier truth-table fixtures retyped to the new diagnostics; UPSERT execution tests inDmlBulkMutationsExecutionTest,GraphQLQueryTest, andSingleRecordCarrierDmlTest@Disabledwith R145 reference. Tests: pipeline-tierR144_*rows onGraphitronSchemaBuilderTest.MutationDmlCase(PK-coverage admission/rejection,multiRowadmission,@value-on-DELETE rejection,multiRow-on-INSERT rejection,@value`@condition+` mutual-exclusion) plusUPDATE_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 proofDmlBulkMutationsExecutionTest.deleteFilmsByReleaseYear_multiRowBroadcastsAcrossInputCardinalityasserts|affected rows| == 3while|input rows| == 1against 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 inresolveInputrejects 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 onresolveInputrecording the parser-level guarantee. Docs: newdocs/manual/reference/directives/value.adoccovers the@valuesurface, per-verb validity rules, and the cardinality-safety interaction withmultiRow;DirectiveDocCoverageTestgreen. 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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R43 commit 5 (
ChildField.RecordTableMethodFieldDTO-parent emit; lift toIMPLEMENTED_LEAVES; R43 In Progress → In Review):SplitRowsMethodEmitter.buildForRecordTableMethodemits the DataLoader rows-method for the new variant: parent VALUES table over the FK source-side columns + the developer’s static@tableMethodcall substituted for the terminalTables.<X>.as("alias")declaration + flatSELECTwithJOIN parentInput ON terminal.<targetSide> = parentInput.<sourceSide>+ scatter viascatterByIdx(list cardinality) orscatterSingleByIdx(single cardinality / LOAD_MANY). The newRowsMethodBody.SqlRecordTableMethodsealed permit gives the body the same framing as the existingSqlRecordTable*siblings (RowsMethodSkeletonemits 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>(takingreturnType: ReturnTypeRef.TableBoundReturnTypeas a parameter) so the existing record-parent DataFetcher emit is shared across the three variants without an interface widening;RecordTableField/RecordLookupTableFieldcall sites thread theirreturnType()explicitly. ThescatterByIdxhelper-emission gate gains aRecordTableMethodField-with-list-cardinality-non-single arm; the existingBatchKeyField.emitsSingleRecordPerKeygate already coversscatterSingleByIdx. Dispatch lift:RecordTableMethodFieldmoves fromSTUBBED_VARIANTStoIMPLEMENTED_LEAVES; the deferred-slug entry retires;generateTypeSpec’s switch arm flips from `builder.addMethod(stub(f))to the pairedbuildRecordBasedDataFetcher+SplitRowsMethodEmitter.buildForRecordTableMethodcalls (mirroringRecordTableField). The variant overridesemitsSingleRecordPerKey()to fold single-cardinality fields onto the single-record-per-key arm, same shape asRecordTableField’s override. Path shape coverage: single-hop `JoinStep.FkJoinis the shipped emit form (the common case, and the only one exercised by the planned pipeline + execution coverage); multi-hop FK paths andJoinStep.ConditionJointerminals surface a runtimeUnsupportedOperationExceptionwith a labelled message, mirroring the table-parentTableMethodFieldcommit-3 emit. Pipeline tests:TableMethodFieldPipelineTest.dtoParentFkAutoDerive_emitsDataLoaderFetcherAndRowsMethodpins the FilmRecord-backed@recordparent + auto-FK-derive + explicit@referencepath shape (the generatedFilmDetailsFetchers.languageDataFetcher signature isCompletableFuture<DataFetcherResult<Record>>wiring aDataLoader/rowsLanguagecall; the rows method body invokesTestTableMethodStub.getLanguage, buildsparentInput, and joins onLANGUAGE_ID).RowsMethodSkeletonTest.rowsMethodBody_sealedSwitchIsExhaustivecount increments to six (the new permit), andUnifiedEmissionPinsTest.rowsMethodEmitter_unifiedSkeletoncount increments to six (the new entry method emits twoRowsMethodSkeleton.buildcalls: 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_routesThroughRecordTableMethodFieldDtoParentEmitexercises the end-to-end DTO-parent path againstrewrite_test. Sakila fixture additions: a new SDL typeFilmDetailsForMethod @record(record: FilmRecord)withfilmId,languageId(declared so the parent SELECT projectsfilm.language_id), andlanguageViaTableMethod: Language @tableMethod(...) @reference(path: [{key: "film_language_id_fkey"}]); a newFilm.detailsForMethod: FilmDetailsForMethodfield that’s aConstructorFieldpassthrough 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 seededlanguage_id=1/name="English"for both films, confirming the developer’stableMethodLanguage()table is correctly joined against the lifted FK keys. Out of scope (called out, not regressed): multi-hop FK path emit andConditionJointerminal emit forRecordTableMethodField(the runtime stubs are loud, not silent); execution-tier coverage for the@sourceRowarm (a separate fixture with a hand-written lifter is a candidate follow-up; the classifier branch is already covered byRecordTableMethodFieldCase). R43 moves In Progress → In Review with this commit. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 (1683 graphitron tests + sakila-example compile + execute tiers all passing). -
R43 commit 4 (
ChildField.RecordTableMethodFieldvariant added, emit stubbed): New sealed-permit onChildFieldcovers child@tableMethodfields on@record(non-table) parents ; the DTO-parent sibling ofChildField.TableMethodField. The record carriesparentTypeName,name,location,ReturnTypeRef.TableBoundReturnType returnType,List<JoinStep> joinPath,MethodRef method,SourceKey sourceKey,LoaderRegistration loaderRegistration,Optional<ErrorChannel> errorChanneland implementsChildField, 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 forTableMethodField/QueryTableMethodTableField). Classifier:FieldBuilder.classifyChildFieldOnResultTypegrew a new@tableMethodbranch placed before the@sourceRowbranch, so both directives can coexist on the same field (their roles are complementary:@sourceRowprovides the batch-key lifter;@tableMethodprovides the developer’s static jOOQ table method). Two admit arms: (a) JooqTableRecordType parent + unique catalog FK between parent’s table and@tableMethodreturn-type table ; auto-derives theSourceKeyvia the existingderiveFkRecordParentSourcehelper, sameWrap.Row+Reader.ColumnReadshapeRecordTableField’s FK arm produces; (b) free-form DTO parent (PojoResultType` /JavaRecordType) +@sourceRow(className:, method:); delegates toSourceRowDirectiveResolverfor the lifter-derivedSourceKey(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@sourceRowand without FK metadata produces a structuredUnclassifiedFieldAUTHOR_ERROR enumerating the three lift options (typed jOOQ TableRecord backing,@sourceRow, or a typed accessor). Dispatch / emit:STUBBED_VARIANTSgains an entry keyed onRecordTableMethodField.classwith plan slugtablemethod-child-table-bound(commit 5 will lift it toIMPLEMENTED_LEAVES);generateTypeSpec’s child switch arm routes `RecordTableMethodFieldthroughstub(f)so schemas exercising the variant fail at validate-time with the standard deferred message rather than crashing at request time. Validator:GraphitronSchemaValidatorgained a new switch arm +validateRecordTableMethodFieldhelper applying the existingvalidateReferencePath+validateCardinalitychecks (mirror ofvalidateTableMethodFieldfor the table-parent sibling). Carrier-plumbing:MappingsConstantNameDedup.withResolvedChannelrebuilds the new variant via its existingWithErrorChannelsealed switch. Tests: newRecordTableMethodFieldCaseenum inGraphitronSchemaBuilderTestpins three classifier shapes ;JOOQ_TABLE_RECORD_PARENT_AUTO_FK(FilmRecord parent +getInventoryauto-FK to Inventory),JOOQ_TABLE_RECORD_PARENT_EXPLICIT_REFERENCE(FilmRecord parent +getLanguagewith explicit@reference(path: [{key: "film_language_id_fkey"}])), andFREE_FORM_PARENT_NO_SOURCEROW_REJECTED(DummyRecord parent +getInventorywithout@sourceRowor FK metadata → rejection naming the three lift options).TestTableMethodStubgainsgetInventory()returningInventory.classfor the new tests.VariantCoverageTestconfirms the new sealed leaf has classification coverage; the existing partition tests (GeneratorCoverageTest.everyGraphitronFieldLeafHasAKnownDispatchStatusandnotImplementedReasonsContainsOnlyConcreteSealedLeaves) stay green because the new class is keyed inSTUBBED_VARIANTSrather thanIMPLEMENTED_LEAVES. Out of scope (kept under R43 commit 5): DTO-parent emit reusingRecordTableField’s DataLoader-keyed batch pattern with the developer’s static method substituted; pipeline-tier fetcher emission tests; execution-tier coverage against `rewrite_test; moveRecordTableMethodFieldfromSTUBBED_VARIANTStoIMPLEMENTED_LEAVES. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 (1673 graphitron tests passing). -
R43 sub-commit (FK-projection injection for child
@tableMethodon table-bound parents): Closes the execution-tier gap commit 3 left open. The child@tableMethodfetcher readsparentRecord.get(DSL.name("<sourceSqlName>"), …)for parent-row correlation; without injecting the FK source-side column into the parent SELECT, the read throwsIllegalArgumentException: Field "<col>" is not contained in row type ("<schema>"."<parentTable>"."<pk>")whenever the user’s SDL selection omits the FK column. Fix:TypeClassGenerator.collectSourceKeyColumnsgeneralises tocollectRequiredProjectionColumnsand gains aChildField.TableMethodFieldarm that extracts the single-hopJoinStep.FkJoin’s `sourceSideColumns()and threads them through the existingrequiredProjectionColumnspipeline ; sameif (!fields.contains(table.$L)) fields.add(table.$L)idempotent-append idiom Split* fields already use for theirSourceKeycolumns. Only single-hopFkJoinshapes contribute: multi-hop andConditionJoinpaths surface a runtimeUnsupportedOperationExceptioninbuildChildTableMethodFetcheranyway, so projecting their first hop would synthesise dead columns. NestingField recursion preserved so nested@tableMethodfields under a non-table-bound nested type get their FK columns into the outer table-class’s$fields. Pipeline tests:TableMethodFieldPipelineTestgainssingleFkAutoInferred_parentDollarFieldsProjectsFkSourceColumn(auto-FKInventory→FilmpinsFILM_IDonInventory.$fields) andexplicitReferencePathSingleHopFk_parentDollarFieldsProjectsFkSourceColumn(explicit@reference(path: [{key: "film_language_id_fkey"}])pinsLANGUAGE_IDonFilm.$fields); both use the sharedTypeSpecAssertions.appendsRequiredColumnhelper that already pins the same idiom for Split* fields. Execution tests:GraphQLQueryTestgainsinventoryById_filmViaTableMethod_correlatesParentRowViaInjectedFkProjection(threeInventoryrows each correlating to their matchingFilmbyinventory.film_id; assertsfilm.filmIdandfilm.titleper row) andfilmById_languageViaTableMethod_correlatesParentRowViaExplicitReferencePathFk(twoFilmrows each correlating toLanguageviafilm.language_id; assertslanguage.languageId == 1and strippedlanguage.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 newChildField.RecordTableMethodFieldvariant for DTO-parent batching (commit 4); DTO-parent emit + execution coverage (commit 5). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R43 commit 3 (child table-bound-parent lift:
ChildField.TableMethodFieldmoves toIMPLEMENTED_LEAVES): NewTypeFetcherGenerator.buildChildTableMethodFetcheremits a per-row fetcher modelled on the root-sitebuildQueryTableMethodFetcher: declareparentRecord = (Record) env.getSource(), call the developer’s static@tableMethodto derive the target table local, declaredsl, build a parent-correlationConditionfrom the resolvedJoinStepchain, then SELECT the typed$fieldsprojection from the developer-returned table with the correlation as WHERE. The new helperbuildTableMethodParentCorrelationwalks eachJoinSlotof the (single)FkJoinhop and emitstable.<targetSide>.eq(parentRecord.get(DSL.name("<sourceSqlName>"), <columnClass>.class)), threading the typedparentRecord.get(name, Class)overload so the resultingConditiontype-checks againstField<T>.eq(T). Composite FKs AND across slots; an empty-slots fallback emitsDSL.noCondition()so the catalog-unavailable case fails loudly at runtime. Path shape coverage: single-hopJoinStep.FkJoinis the shipped emit form (the common case, and the one exercised by R43’s planned pipeline + execution coverage). Multi-hop FK paths andJoinStep.ConditionJointerminals are still accepted by the classifier (GraphitronSchemaBuilderTest.TableMethodFieldCase.LIST_RETURN/CONNECTION_RETURN/WITH_CONDITION_PATH) but the emitter surfaces a runtimeUnsupportedOperationExceptionwith the shape label ("empty joinPath"/"multi-hop join path"/"ConditionJoin path") so the gap is loud rather than silent. Three@DependsOnClassifierCheckannotations pin the producer/consumer contracts (tablemethod-resolver-return-is-table-boundfor the narrowed return type,service-catalog-strict-tablemethod-returnfor the no-downcast emit,service-catalog-tablemethod-must-be-staticfor the static-call shape) plusfk-join.slots-oriented-source-and-targetonbuildTableMethodParentCorrelation. Dispatch:IMPLEMENTED_LEAVESgainsChildField.TableMethodField.class;STUBBED_VARIANTSloses its entry (thetablemethod-child-table-boundplan-slug binding retires);generateTypeSpec’s child switch arm flips from `builder.addMethod(stub(f))tobuilder.addMethod(buildChildTableMethodFetcher(ctx, f, outputPackage)). Pipeline tests: newTableMethodFieldPipelineTesttwo cases:singleFkAutoInferred_emitsFetcherMethod(Inventory.film with single-FK auto-inference) andexplicitReferencePathSingleHopFk_emitsFetcherMethod(Film.language with@reference(path: [{key: "film_language_id_fkey"}])); both assert the generatedFilmFetchers/InventoryFetcherscontain a fetcher method under the field name with the standard(DataFetchingEnvironment)signature, the correctDataFetcherResult<Record>return type, and a body that invokes the developer-authored static method.TableMethodFieldValidationTest’s three `stubbedErrorcases (NO_PATH,WITH_FK_PATH,WITH_CONDITION_ONLY) flip to assert empty errors ;TableMethodFieldis no longer inSTUBBED_VARIANTS, soGraphitronSchemaValidator.validateVariantIsImplementedis silent for these fixtures. Compile-tier coverage: sakila-example schema gainsInventory.filmViaTableMethod: Film @tableMethod(...)(auto-FK single-hop) andFilm.languageViaTableMethod: Language @tableMethod(...) @reference(path: [{key: "film_language_id_fkey"}])(explicit single-hop path);SampleQueryServiceaddstableMethodFilm()/tableMethodLanguage()returningTables.FILM/Tables.LANGUAGEdirectly; thegraphitron-sakila-examplecompile 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@tableMethodfield is in the selection set; today the parent’s$fieldswalks only user-requested SDL fields andparentRecord.get(DSL.name("film_id"), …)fails withIllegalArgumentException: Field "film_id" is not contained in row type ("public"."inventory"."inventory_id"). Mechanism for FK-column injection (analogous to the projection synthesis thatNodeIdReference/CompositeColumnReferencealready 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 newChildField.RecordTableMethodFieldvariant for DTO-parent batching (commit 4); DTO-parent emit (commit 5). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 (1640+ graphitron tests + sakila-example compile + execute tiers all passing). -
R43 commit 2 (path resolution + last-hop-target validation for
@tableMethodat child sites):FieldBuilder.classifyChildFieldOnTableType’s `@tableMethodarm reorders the resolver andparsePathcalls so the return-type table is known before path resolution:tableMethodResolver.resolveruns first, thenctx.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 inparsePath(findForeignKeysBetweenTableswithdirectiveAbsent=true). The classifier adds a last-hop-target check: if the resolved path is non-empty and its last hop is aJoinStep.FkJoin, the hop’stargetTable().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.ConditionJoinlast hops are exempted from the structural check by design ; the condition method’s signature is the implicit contract there. Three accepted shapes (matching@referencesemantics): (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 existingfkCountMessage"multiple foreign keys found between tables …" with directive-absent guidance; missing FK + no@referencesurfacesfkCountMessage’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_ARGSadd explicit@reference(path: …)(Film→Languagehad two FKs and would now reject as ambiguous;Film→Actorhas 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 onfilm);TableMethodFieldCase.WITH_CONDITION_PATH(@reference(path:[{condition:…}])resolves to ConditionJoin). New rejection tests inUnclassifiedFieldCase: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 declaredActor→ last-hop-lands-on-language rejection). No emit change:TypeFetcherGenerator.STUBBED_VARIANTSstill mapsChildField.TableMethodField.classto the deferred slug; the dispatch ingenerateChildFetcherstill routes tostub(f). The lift toIMPLEMENTED_LEAVESis R43 commit 3. Out of scope: emit (commit 3); the newChildField.RecordTableMethodFieldvariant for DTO-parent batching (commit 4); DTO-parent emit (commit 5). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R43 commit 1 (directive flattening + method-signature rewrite at the root site): Retired
BuildContext.ARG_TABLE_METHOD_REF; flattened the@tableMethoddirective indirectives.graphqlsfrom(tableMethodReference: ExternalCodeReference!, contextArguments:)to(className: String!, method: String!, argMapping: String, contextArguments: [String!]), mirroring@sourceRow.TableMethodDirectiveResolver.resolveparses the flat args inline (sibling toSourceRowDirectiveResolver);parseExternalRefstays as-is for@service/@externalField. Method-signature contract: developer’s@tableMethodstatic method now receives only GraphQL field arguments andcontextArguments:values ; no Table parameter.ServiceCatalog.reflectTableMethodgains a newTableSlotPolicy { REQUIRED, FORBIDDEN }parameter so the three call sites (TableMethodDirectiveResolver, two@conditionpaths inConditionResolver, two@conditionpaths inBuildContext.parseCondition*) pick their semantics:@tableMethodpasses FORBIDDEN (reject anyTable<?>parameter; nofoundTablerequirement),@conditioncallers pass REQUIRED (keep the originalParamSource.Tableslot +foundTableinvariant + the reserved-Table-slot argMapping typo guard, factored as the newcheckConditionOverrideTargetshelper).TypeFetcherGenerator.buildQueryTableMethodFetchernow passesnullfortableExpressiontoArgCallEmitter.buildMethodBackedCallArgsand the emitted call drops the leadingTables.<NAME>argument; the body shape staysvar table = ClassName.method(<args>)with<args>now sourced exclusively fromParamSource.Arg/ParamSource.Contextslots. The flat form drops the deprecatedname:alias on@tableMethod(the existing@sourceRowprecedent 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 inGraphitronSchemaBuilderTest,ServiceRootFetcherPipelineTest,TableMethodFieldValidationTest,DiagnosticsTest,ClassNameCompletionsTest, and the sakila-example schema flattens to@tableMethod(className: "X", method: "Y").TestTableMethodStubstatic methods drop their leadingTable<?>parameter.SampleQueryService.popularFilmsrewrites from(Film filmTable, Double minRentalRate)to(Double minRentalRate), derivingTables.FILMinternally.ServiceCatalogTest’s `reflectTableMethod_*cases thread the new policy parameter;reflectTableMethod_overrideTargetingTableSlot_rejectednow exercises the REQUIRED policy viaTestConditionStub.argCondition.TypeFetcherGeneratorTest.queryTableMethodTableField_emittedFetcher_*drops theParamSource.Tableslot from its handcraftedMethodRef. The LSPlegacyName_unresolved_tableMethodtest retires (name:alias is gone). User-facing docdocs/manual/reference/directives/tableMethod.adocrewrites 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.adocupdates the per-directive slot-name table to distinguish flat-form (@tableMethod,@sourceRow) fromExternalCodeReference-shaped directives. R43 status was moved Ready → In Progress in55b5d5fahead of this commit. Out of scope (kept under remaining R43 commits): path resolution + last-hop-target validation; child table-bound-parent lift (TypeFetcherGenerator.STUBBED_VARIANTSmembership forChildField.TableMethodFieldremains); newChildField.RecordTableMethodFieldvariant. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R100 (
362719eimplementation;8498b89In Progress → In Review): LSP coverage for@node(keyColumns:)and@nodeId(typeName:). Per-keystroke responsiveness layer on top of the existing classifier-rejection paint (validatorDiagnosticskeeps full coverage at the rebuild tier). Two canonical-overlay deltas plus one newBehaviorarm:@node(keyColumns:) → CatalogColumnBindingsoFieldCompletionsandDiagnostics.validateCatalogColumnauto-fire over the type’s@table-backed jOOQ column list;@nodeId(typeName:) → new Behavior.NodeTypeBinding()siblings-by-keyset toCatalogColumnBinding/CatalogTableBinding, with one new completion provider (NodeTypeCompletions) and one new arm inDiagnostics.dispatchthat reads a newCompletionData.nodeMetadata()map.CatalogBuilderwalks the assembled schema’sGraphQLObjectType`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/descendLeavesfan out rawlist_valueAST nodes into oneLeafper scalar element soCatalogColumnBindingdispatches per-element onkeyColumns: [...]; the contract pin “Leaf.valueNode` is the scalar value node, never an enclosinglist_value” lifts to a universal property of the leaf walk. `Hovers.valueNodeFormirrors the descent so cursor inside a list element highlights the element, not the whole list; newnodeTypeHoverarm renders the target type’stypeId+ key-column list with each column’sgraphqlTypepulled fromCompletionData.Column. Two backwards-compatCompletionDataconstructors 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 inNodeTypeCompletionsTest(3); leaf-walk fan-out pin inLspVocabularyTest.leafCoordinates_listValueFansOutOneLeafPerElement; catalog-sideNodeMetadatacoverage inCatalogBuilderTest(3 cases ; author-supplied capture, omitted-axes-stay-null, non-@nodetypes omitted). Reference docs gain "Editor support" subsections onnode.adocandnodeId.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;@nodeplacement 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.columnGraphqlTypedoes case-insensitive across all tables rather than scoping to the@nodetype’s@table(latent, not triggered by Sakila; candidate follow-up). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R141 (
d58d46cIn Progress → In Review carrier-shape lift + new sealed leaf + tests;83cd67arework: order-preservation PK-keyed-map indirection inFetcherEmitter.buildSingleRecordTableFetcherValue’s `Cardinality.MANYarm): Admit bulk-input mutations with a single payload carrier wrapping a list-shaped data field. NewMutationField.MutationBulkDmlRecordFieldsealed leaf (sibling ofMutationDmlRecordField) classifies(tia.list() == true, dataField.wrapper().isList() == true, kind ∈ {INSERT, UPDATE}); carrier-shape lift introduces sealedCarrierFieldRole(permitsDataChannel,ErrorChannelRole) withSingleRecordCarrierShapecarryingList<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 inBuildContext.tryResolveSingleRecordCarrier(the carrier-sideresolveErrorChannelcall 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 insideMutationInputResolver.validateReturnType. Emit strategy: per-row DML insidedsl.transactionResult(...)accumulating PKs intoResult<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 inFetcherEmitter.buildSingleRecordTableFetcherValue’s `Cardinality.MANYarm (re-key SELECT result intoMap<PK, Record>, iterate the upstream input-orderedResult<RecordN<PK>>to project intoList<Record>in input order). UPDATE no-match throwsIllegalStateExceptionto keepacc.size() == in.size()invariant. Audit. New load-bearing classifier-check keysingle-record-carrier-shape.roles-exhaustively-classified(producer ontryResolveSingleRecordCarrier, consumers onGraphitronSchemaBuilder.registerCarrierDataFieldandTypeFetcherGenerator.buildMutationBulkDmlRecordFetcher);mutation-dml-record-field.data-table-equals-input-tableextends across both record-carrier leaves. NewCarrierFieldRoleCoverageTestaudits 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 inDmlBulkMutationsExecutionTest:bulkInsertWithThreeRowsInNonPkOrderPreservesInputOrderInResponse(N=3 load-bearing order assertion),bulkInsertWithSingleRowExercisesBulkLeafPath(N=1 sanity),bulkUpdateWithThreeRowsInNonPkOrderPreservesInputOrderInResponse(UPDATE order assertion). Sakila fixture gainsFilmsPayload { films: [Film!] }+createFilmsPayload/updateFilmsPayloadmutations. Out of scope (deferred): per-row error correlation (R12 flat-error contract preserved); affected-row-count / clientMutationId sibling permits (each is a newCarrierFieldRolepermit + classifier rule); UPSERT bulk-carrier admission (R145);@servicebulk-carrier symmetric path; sealed-on-kind / sub-taxonomy refactors of both record-carrier leaves. Defers consumer-side@DependsOnClassifierCheckannotations against R12’s plannederror-channel.*keys; R12 lands both halves in one commit when it ships, referencing R141’sErrorChannelRolepermit Javadoc as the trust-statement anchor. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R150 (
aa7e1b0implementation;c8bdbe5SDL-driven invariant tightening;e96a05cSDL Scalar vs Input Object only;bfdde5epermanent Map rejection;2c2b37acycle detection + public-class check;bb79b63polish ; typedLinkageError/ClassNotFoundException, nested-class.→$retry,createBean/createBeanListplural naming, FieldKey inlined): Instantiate service-layer input beans at the@servicefetcher boundary. Closes the silentClassCastException: LinkedHashMap cannot be cast to <ConsumerBean>gap when a@servicemethod’s Java parameter (single orList<Bean>) is a consumer-authored class mirroring an SDLinputtype. NewCallSiteExtraction.InputBeansealed-variant arm carries the beanClassName, theTarget(RECORD/JAVA_BEAN constructor shape), and per-SDL-fieldFieldBinding`s. `InputBeanResolverpost-processes a resolvedMethodRef.Service(sibling toEnumMappingResolver.enrichArgExtractions), driving classification off the SDL side: GraphQL scalar args (including custom scalars wired via@scalarType) stay onDirectso graphql-java’s coercion delivers the consumer’s declared Java type, and GraphQL input-object args classify asInputBeanor 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-scopedvisitedset, preventsStackOverflowErrorat 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.InputBeanInstantiationEmitteremits onecreateBean(Map<String, Object>)+ onecreateBeanList(Object)helper per unique bean class on the enclosing*Fetchersclass ; dedup-by-class viacollectTransitivelywalks nested input-object leaves. Records use positional canonical-ctor; JavaBeans use no-arg +set<X>setters.ArgCallEmitter.buildArgExtractionroutes theInputBeanarm 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: L1TypeFetcherGeneratorTestpins helper signature + record/JavaBean target +createFooListplural naming + transitive dedup; L2GraphitronSchemaBuilderTestcovers singular/list InputBean classification plusSERVICE_MAP_PARAM_FOR_INPUT_OBJECT_REJECTED,SERVICE_RECURSIVE_BEAN_REJECTED,SERVICE_NON_PUBLIC_BEAN_REJECTEDarms; L3graphitron-sakila-exampleaddsFilmReviewDetailsInput+FilmReviewTagInputSDL types andsubmitFilmReviewWithDetails(details: FilmReviewDetailsInput!)mutation, with consumer-authoredFilmReviewDetailsrecord +FilmReviewTagrecord ingraphitron-sakila-servicecompiling against the generated helper; L4GraphQLQueryTest.submitFilmReviewWithDetails_routesThroughInstantiatedInputBeanround-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):@serviceparameter as a jOOQTableRecordsubclass currently routes through the JavaBean setter path (not idiomaticrecord.from(map); tracked as a follow-up); recursion is head-only ; a@serviceparameter whoseargMappingis a multi-segment dot-path stays on the legacyDirectarm even when the leaf SDL type is an input object; SDLoneOfpolymorphic inputs and builder-pattern target classes deliberately deferred. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R108 (
a60c58aimplementation): Per-variant projection on polymorphic fields. The multi-table polymorphic dispatcher’s Stage-2 per-typename SELECT now threads the parentDataFetchingFieldSelectionSetthroughPolymorphicSelectionSet.restrictTo(source, concreteTypeName)(new emitted helper at<outputPackage>.util.PolymorphicSelectionSet, generated byPolymorphicSelectionSetClassGeneratorundergenerators/util/, registered inGraphQLRewriteGeneratornext toConnectionHelper), so each per-typename SELECT projects only columns whoseSelectedField.getObjectTypeNames()contains that participant. The helper is a delegating wrapper that materially overrides onlygetFieldsGroupedByResultKey()and delegates every otherDataFetchingFieldSelectionSetmethod to the source, keeping the nested-projection recursion in$fields(which walkssf.getSelectionSet()) intact and avoiding a widened$fieldssignature. Same-table interface emit site atTypeFetcherGenerator.buildInterfaceFieldsListintentionally untouched (per-spec carve-out: theLinkedHashSetdedup masks over-selection in every currently-exercised fixture); javadoc cross-reference notesrestrictTois reusable as-is when a fixture exercises the break-the-dedup shape. Tests four-tier:PolymorphicProjectionFilterPinTest(unit,UnifiedEmissionPinsTestprecedent ; folder-wide$T.restrictTo(env.getSelectionSet()count == 1, single-file$$fields(env.getSelectionSet()inMultiTablePolymorphicEmitter.javacount == 0);PolymorphicSelectionSetClassEmitTest(pipeline, structural pin of the emitted class ; name, modifiers,restrictTosignature, private no-arg constructor, private static finalFilterednested type implementingDataFetchingFieldSelectionSet);PolymorphicNestingFilterTest(pipeline, asserts exactly onePolymorphicSelectionSetreference per Stage-2 helper body, encoding "no further filter needed at depth");RecordParentMultiTablePolymorphicPipelineTestextended with an asymmetric-fragment fixture (Inventory + Content sharingfilmIdbacked by different columns on different tables) driving full SDL → classify → emit;PolymorphicProjectionQueryTest(execution, SQL-capture via jOOQExecuteListener; 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__/idxprojections all untouched (added outside the$fieldscall or inside$fieldsoutside the selection switch). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R147 (
4fadc3dimplementation;2450971simplify: drop the backward-compatDiagnostics.computeoverload): SurfaceGraphitronSchemaValidatorerrors 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 newValidationReport(errors, warnings, sourceUris)record alongside aBuildArtifacts(catalog, snapshot)split ofBuildOutput; the precomputedsourceUriscanonical-URI set letsDiagnostics.computeshort-circuit per file with oneSet.contains.Workspace.setBuildOutput(BuildArtifacts, ValidationReport)replaces the oldsetCatalog/setCatalogAndSnapshotoverloads, atomically swapping all three volatile refs;DevMojoroutes both the schema-save and classpath triggers through the unified setter, so unresolved-@service-class errors surface in the editor on the nextmvn compilewithout waiting for a schema save. Severity mapping is an exhaustiveswitchover theRejectionsealed hierarchy (AuthorError/InvalidSchema→Error,Deferred→Warning);BuildWarningmaps toWarning. Freshness-aware silence policy mirrors R139: validator diagnostics fire underBuilt.Currentonly, silent underUnavailableandBuilt.Previous. Source attribution is"graphitron-validator", distinct from"graphitron-lsp". Two new paired classifier-check keys (source-location.absolute-path-source-nameonRewriteSchemaLoader,validation-report.canonical-urionValidationReport.canonicalUri) pin the cross-module invariants the LSP filter relies on. Tests:ValidatorDiagnosticsTestcovers severity perRejectionpermit, per-file filtering, freshness gating, no-usable-location drop, and the empty-report-clears-previous-diagnostics contract at compute-call level;RejectionSeverityCoverageTestpins exhaustiveness reflectively;WorkspaceTestgetssetBuildOutputswap tests;ValidationReportTestcovers thefromfactory and the canonical-URI helper;CatalogRefreshTestmigrates 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 LSPpublishDiagnosticswire-test andGraphQLRewriteGeneratorTestforbuildOutput()report population were deferred. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R143 (
e670fb9): Surface a date column on the rolled-up roadmap.Itemgains nullablecreated: LocalDate/lastUpdated: LocalDateparsed viaItem.from.parseDate(accepts both SnakeYAML’s auto-parsedjava.util.Dateshape and bare-string YAML; absent passes, malformed throws naming slug+key+value).runCreatestamps both dates today; newstatussubcommand (runStatus+ the pure, package-visibleapplyStatusTransition) resolves slug orR<n>viaresolveItemFile, validates target+transition againstTARGET_STATES/ALLOWED_TRANSITIONS, writes newstatus:+ freshlast-updated:, leavescreated:strictly untouched (never invented for pre-R143 items), and regenerates README;DoneandDiscardedare rejected as targets perworkflow.adoc. MarkdownrenderActivegains anUpdatedcolumn;appendBacklogLineemits<sub>updated Y-M-D[, created Y-M-D]</sub>between description andblocked by:. AsciiDocrenderAdocStatusBoardbecomes[cols="1,4,1,1,1"]with the new column; backlog adoc emits italic_(updated ...)_; the plan-page attribute box gainsCreated/Updatedrows (suppressed when absent)..claude/skills/roadmap/SKILL.mdrewritten to invoke thestatussubcommand instead of hand-editing front-matter;workflow.adocgains 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 ofDone/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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R140 (
036772f): Publish leaf-coverage report from CI.rewrite-build.ymlbuildjob grows two trunk-gated steps aftermvn verify -Plocal-db;Regenerate leaf-coverage reportruns the roadmap-tool exec against the in-workspacetarget/leaf-coverage.jsonltraces, thenUpload leaf-coverage artifactuploads the regeneratedgraphitron-rewrite/roadmap/inference-axis-coverage.adocas theinference-axis-coverageartifact withif-no-files-found: error. Two new jobs in the same workflow:docs-build(needsbuild, trunk-gated, downloads the artifact over the committed placeholder, builds the docs module with-DskipTests, uploads the Pages artifact) anddocs-deploy(needsdocs-build, trunk-gated,pages: write+id-token: write,pagesconcurrency,github-pagesenvironment,actions/deploy-pages@v4)..github/workflows/deploy-docs.ymldeleted; the consolidation sidesteps theworkflow_run-on-default-branch constraint that blocked the original R132 sketch (rewrite workflow files do not live onmain).workflow_dispatchdoes not survive; manual re-deploy is via the Actions UI re-run on the most recent successful trunk run.inference-axis-coverage.adocprose 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, theRegenerate leaf-coverage reportstep here will need-Pleaf-coverageadded; 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 againsthttps://sikt-no.github.io/graphitron/roadmap/inference-axis-coverage.html. -
R142 (
04a649buser-directive arms wired throughDirectiveResolution;4ac157eself-review fixes pinning bundled-shadows-snapshot precedence on the hover surface;a39ce93In 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 sealedDirectiveResolution.{Bundled | User | Unknown}result and light up on user-declared directives:Hovers.computesurfaces directive-name hovers (pre-coordinate branch ondirective.nameNode()) and arg-name docstring fallback fromInputValueShape.description();Diagnostics.computeextends its existing outer-snapshot / inner-resolution switch onBuilt.CurrentwithvalidateUnknownArgsAgainstSnapshot+validateRequiredArgsAgainstSnapshotpackage-private helpers next to the bundled equivalents;ArgNameCompletions.generategrows anLspSchemaSnapshotparameter and routes the User arm through a top-level-onlyuserGeneratehelper (nested completion stays empty until the snapshot carries input-object shapes). Hovers and completions are freshness-agnostic (stale info beats silence); diagnostics warn only underBuilt.Current(mirrors R139’s unknown-directive arm). Bundled-shadows-snapshot precedence (R139 settled design note 4) pinned with parallel guards on all three consumers:Hoversgates user-arm fallback onresolution instanceof DirectiveResolution.User,Diagnosticskeeps the existingBundledearly-continue, and the newbundledDirectiveArgHover_ignoresSnapshotShadow/bundledDirectiveShadowedBySnapshot_routesThroughBundledPathcases anchor the guard symmetrically withDiagnosticsTest.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 threadsLspSchemaSnapshot.unavailable()for tests not exercising the user-arm. No unit-tier additions (each consumer is a thin walker over the recordsCatalogBuilderSnapshotTestalready pins; seal exhaustiveness isjavac-checked); no schema-fixture additions tographitron-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@DependsOnClassifierCheckmarkers ;Hovers.computeandArgNameCompletions.generateare freshness-agnostic so no classifier guarantee is load-bearing for them, andDiagnostics.compute’s existing marker still covers the new arg-validation arms (they inherit the same `Built means clean parsedependency through the samecomputebody). 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 inDiagnosticsand nested arg-name completion inArgNameCompletionstogether as a producer-side widening); lift the directive-name hover branch intoLspVocabularyonce a third consumer wants it. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R138 (
a5bc310In Progress → In Review implementation;bb415a2plan-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’sNoBacking-promotion carrier path, bypassing the out-of-band deferred rejection inFieldBuilder.buildDmlField; the generated fetcher ended invaluesOfRows(...).returningResult(...).fetchOne(), which throwsTooManyRowsExceptionfor every input with >1 row. Fix lifts thelistInput && !returnType.wrapper().isList()predicate toMutationInputResolver.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 nameTooManyRowsExceptionas 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 `listInputparameter and the four kind-switch call sites.TypeFetcherGenerator.buildMutationDmlRecordFetchercollapses to a single emit shape: theif (tia.list())empty-list short-circuit block, thedataIsListlocal, thepayloadTypeternary, and the.fetch()/.fetchOne()terminator ternary all retire; the fetcher emits a singlerowType RecordN<...>via unconditional.fetchOne(), and the Javadoc is rewritten to describe the one remaining shape.graphitron-sakila-example/schema.graphqlsdrops thecreateFilmsPayloaddeclaration 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. NewGraphitronSchemaBuilderTest.DML_INSERT_LIST_PLAIN_PAYLOAD_REJECTEDrow covers the plain-SDL carrier variant; the pre-existingDML_INSERT_LIST_PAYLOAD_DEFERREDrow renames toDML_INSERT_LIST_PAYLOAD_REJECTEDand 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_FIELDflips fromcreateFilms(in: [FilmCreateInput!]!)tocreateFilm(in: FilmCreateInput!); fourSingleRecordCarrierPipelineTestfixtures (carrier_listDataField_classifiesAsMutationDmlRecordField,carrier_listDataField_dataFieldClassifiesAsSingleRecordTableField,carrier_atRecordWithNullClassName_classifiesAsMutationDmlRecordField,carrier_withDelete_rejectsAtClassifier) switch frompayloadDml(bulk) topayloadDmlSingleInput; the carrier-promotion and trigger-rejection cases keep bulk input because the per-arm rejection fires first. No execution-tier test (the failure mode isTooManyRowsExceptionthrown insidetransactionResult; 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 slugbulk-input-single-carrier-list-data-fieldif a real schema surfaces a need. Build green:mvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R130 (
57d6673Phases 1–4 implementation;8f42848Phase 4 compile- and execute-tier coverage): Admit the two same-table@nodeId-decoded input-field carriers (InputField.ColumnFieldwithCallSiteExtraction.NodeIdDecodeKeysandInputField.CompositeColumnField) in@mutationinputs and@lookupKeybindings. The headline forcing function is composite-PK DELETE shaped likeslettRegelverksamling(input: { id: ID! @nodeId @lookupKey })against a composite-PK table; post-R131 the same-table arm classifies asCompositeColumnFieldand pre-R130 theMutationInputResolverrejected the carrier outright. Phase 1 (model + extraction-propagation fix): newInputColumnBindingGroupsealed root (MapGroup,DecodedRecordGroup) as a sibling to R50’sLookupArg, rooted at an input-field cluster rather than an outer GraphQL argument;InputFieldgains sealedLookupKeyField/SetFieldsub-interfaces permittingColumnFieldandCompositeColumnFieldonly (reference carriers stay outside the permits set);TableInputArg.fieldBindingsretypes toList<InputColumnBindingGroup>andlookupKeyFields/setFieldsretype to the new sealed permits;EnumMappingResolver.buildLookupBindingshonors the carrier’scf.extraction()when non-Directinstead of unconditionally re-deriving from raw column metadata. The pre-R130 unconditional re-derivation discarded the resolver-suppliedNodeIdDecodeKeys; the R131 follow-up SDL-boundary@nodeIdguard (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@LoadBearingClassifierCheckkeys (mutation-input.lookup-binding-honors-carrier-extraction,mutation-input.lookup-binding-decoded-record-arity-matches-carrier-columns) paired with@DependsOnClassifierCheckconsumers on the lookup-WHERE / row-IN / INSERT-arm emitters. Phase 2 (classifier admission):MutationInputResolveradmitsColumnField(NodeIdDecodeKeys)andCompositeColumnFieldin lookup-bearing verbs; reference-carrier rejections reframe as R24-shapedRejection.deferred(summary, "nodeidreferencefield-join-projection-form");CompositeColumnField × INSERTcarves out viaRejection.deferred(summary, "")(no roadmap item exists today; lifts when a forcing-function schema appears);CompositeColumnFieldoutside@lookupKeyposition 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):buildLookupWhereSingleRowlifts a per-rowRecord<N>decode local topostInGuardwithThrowOnMismatchnull handling (GraphqlErrorException on wrong-type id);buildBulkLookupRowInadopts a block-lambda form for decode-bearing groups (expression-lambda preserved for the all-Directshape so existing pipeline traces stay byte-identical); INSERT / UPSERT column lists expandCompositeColumnFieldinto its N member columns viabuildInsertColumnList;buildPerCellValueListdispatches on carrier identity for the values list;buildInsertDecodeLocalslifts decode locals intopreGuardfor single-row INSERT / UPSERT (per-row inside the stream lambda for bulk). Phase 4 (tests): classifier tests inMutationDmlNodeIdClassificationTestfor composite-PK DELETE / UPDATE / UPSERT admission, INSERT carve-out, and single-PK extraction-propagation; the two R131 follow-upGraphitronSchemaBuilderTest.ArgumentParsingCase.LOOKUP_KEY_ON_NODEID_INPUT_FIELD_REJECTED{,_COMPOSITE_PK}cases retype toLOOKUP_KEY_ON_NODEID_INPUT_FIELD_ADMITTED{,_COMPOSITE_PK}assertingMapInput.bindings[0].extractionisNodeIdDecodeKeys(single-PK) andLookupArg.DecodedRecord.bindingshas the expected positional arity (composite-PK). Phase 4 (compile + execute) (8f42848): sakila-example surfacesDeleteFilmActorByNodeIdInput @table(name: "film_actor")withid: ID! @nodeId(typeName: "FilmActor") @lookupKeyplusMutation.deleteFilmActorByNodeIdandMutation.deleteFilmActorsByNodeIddriving bothbuildLookupWhereSingleRow’s `DecodedRecordGrouparm andbuildBulkLookupRowIn’s block-lambda arm end-to-end; new `keyed_node(id varchar PK, label varchar)table ininit.sqlplusKeyedNode @nodetype andMutation.createKeyedNodedriving theColumnField(NodeIdDecodeKeys)INSERT-arm (buildInsertDecodeLocalspreGuard local +buildPerCellValueListNodeIdDecodeKeysarm); five execution-tier tests inDmlBulkMutationsExecutionTestcovering composite-PK DELETE single-row, composite-PK DELETE bulk row-IN, single-PK INSERT round-trip, andThrowOnMismatchon 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 inMutationDmlNodeIdClassificationTest.compositePkNodeIdLookupKey_{update,upsert}_admitted); reference-carrier admission stays R24-coupled (no forcing-function schema today). Retired key:lookup-key-input-field-non-nodeid-decodedretires producer-only (zero@DependsOnClassifierCheckconsumers;LoadBearingGuaranteeAuditTestsurfaces no orphan). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; 23 tests inDmlBulkMutationsExecutionTestpass, 10 inMutationDmlNodeIdClassificationTestpass. -
R75 (
dbffee9Phase 1 reshape;862bc86Phase 1 direct-@tabletwo-step emit + durability pins;2fd7598Phase 1 follow-up SELECT coverage;88df99aPhase 2 foundation;be26134Phase 2 lean + R137 carve-out;2408634+031c6f3Phase 2 review fixes): Plain payload types for DML mutations. Replaces the earlier wire-format-unwrap design (PassthroughDataFieldpermit +IdentityPassthroughcapability +BuildContext.resolveReturnTypeshort-circuit, all retired) with the structural model the SDL implies: plain SDL Object carriers promote to a newPojoResultType.NoBackingarm at type-classification time, payload-returning DML mutations classify asMutationField.MutationDmlRecordField(DELETE rejected at classify time via the compact constructor), and the data field on the carrier classifies as the newChildField.SingleRecordTableFieldsibling permit with an inlineSourceKey(newReader.ResultRowWalkpermit on R38’s sealedReaderinterface,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-@tableshapes: PK-onlyRETURNINGinsidedsl.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-backedResultTypeelements via a sealedDataElementsub-taxonomy (Table/Record); record-element data on@servicemutations classifies as the newChildField.SingleRecordIdentityFieldpermit (identity-passthrough emit, noSourceKey, no SELECT), and DML mutations reject record-element carriers at classify time. Audit. Two new@LoadBearingClassifierCheckkeys (mutation-dml-record-field.data-table-equals-input-tableandsource-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-tierSingleRecordCarrierPipelineTestcovers per-DmlKindadmission, fullSourceKeyshape,PojoResultTypesplit, DELETE rejection, trigger rejections, table-equality rejection, the structural two-step-emit pin on direct-@tablereturns, the fetcher-emitter arm-count pin, Phase 2’s record-element classification, the parameterised record-element DML rejection (INSERT/UPDATE/UPSERT), and theSingleRecordIdentityFieldarm pin. Execution-tierSingleRecordCarrierDmlTestcovers round-trip for INSERT / UPDATE / UPSERT (new and existing rows) against sakila plus selection-set strength tests (auto-PK, DB default,@referenceprojection, post-UPDATE state read), plus the headline durability pinsdml_persists_when_followupSelect_throws(carrier) anddml_persists_when_directReturnSelect_throws(direct-@table) via the syntheticDurabilityErrorService.synthesizemid-traversal throw.GraphitronSchemaBuilderTest.NonTableParentCasegainsSINGLE_RECORD_CARRIER_DATA_FIELD,SINGLE_RECORD_IDENTITY_FIELD, andMUTATION_DML_RECORD_FIELD. Compilation-tier sakila fixtureMutation.createFilmCard(filmId: Int!): SingleFilmCardCarrierwires 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@servicesubstrate admitting wrapper layers on method return types. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 across all 11 modules. -
R131 (
a64cd8f+1acbaa0+fe2de55+beb0e92): Collapse the singularid: ID! @nodeIdinput-field classifier ontoNodeIdLeafResolver.resolveso both arities (ID!and[ID!]) consume the same sealedResolvedoutcome (SameTable | FkTarget.DirectFk | FkTarget.TranslatedFk | Rejected) through a single shared helperBuildContext.inputFieldFromNodeIdResolved. Pre-R131 the singular branch open-coded typeName inference, schema/catalog lookup, path parse,validateLift, andliftSourceColumns, then funneled every outcome into the Reference-onlybuildInputNodeIdReferencesink ; 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 onCompositeColumnField(same-table arm), notCompositeColumnReferenceField. The duplicateBuildContext.NodeIdTypeNameInferencerecord andinferNodeIdTypeNamehelper retire ;NodeIdLeafResolver.inferTypeNameis the single home.buildInputNodeIdReferencesurvives 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@DependsOnClassifierCheckannotations onBuildContext.classifyInputFieldInternalandFieldBuilder.walkInputFieldConditionsrewritten to describe the post-R131 shape; the stale "`CompositeColumnReferenceField` may represent a same-table PK filter" reading is gone (MutationInputResolverrejects all four@nodeId-decoded input-field carriers as deferred; R130’s post-R131 pivot retains scope for the same-table column-direct half ;ColumnFieldwithNodeIdDecodeKeysandCompositeColumnField, the carriers the post-R131 classifier produces fromSlettRegelverksamlingInput-shaped schemas ; and defers the genuinely-joined*ReferenceFieldhalf per R24’s "wait for forcing-function schema" discipline). Tests:NodeIdPipelineTestfour-corner pipeline pins for singular + (same-table |FkTarget.DirectFk) × (single-PK | composite-PK) ;InputCase.EXPLICIT_NODE_ID_DIRECTIVEupdated toCompositeColumnField, newEXPLICIT_NODE_ID_DIRECTIVE_SINGLE_PK→ColumnField, newInputReferenceCase.REFERENCE_TO_COMPOSITE_PK_NODE_TYPE→CompositeColumnReferenceFieldwith positionally-alignedliftedSourceColumns,NODE_TARGET_NO_METADATA_PK_FALLBACKupdated toColumnField, newArgumentSameTableNodeIdCase.SAME_TABLE_SCALAR_COMPOSITE_PKpinningFieldBuilder.classifyArgumentemitsBodyParam.RowEqover the parent’s PK columns;MutationDmlNodeIdClassificationTest.nodeIdFieldInInput_deferredrejection text shifts toCompositeColumnField. Compilation tier (sakila example): newFilmActorSingularNodeIdFilter @table(name: "film_actor") { id: ID! @nodeId(typeName: "FilmActor") }plusQuery.filmActorBySingularCompositeNodeId(filter:)emitsDSL.row(table.ACTOR_ID, table.FILM_ID).eq(id).CompositeDecodeHelperRegistry.buildHelperdrive-by switches the singular non-list branch fromvar r = ...to a typedRecord<N>declaration via a newtypedRecordhelper (caught byGeneratedSourcesLintTest.varGuardonce the singular composite-PK path reached this branch for the first time). Reachability claims (multi-hop, condition-step, andTranslatedFkrejections) 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-up1acbaa0(FK-target / NodeType-keyColumns permutation):NodeIdLeafResolver.permutationToKeyColumnsreplaces the strictsameColumnsBySqlNamepredicate ; when the terminal hop’s target-side columns equal the NodeType’s@node(keyColumns:)as a multiset (any order), theDirectFkarm permutesjoinPath.liftedSourceColumns()intokeyColumnsorder before constructing the carrier; the@LoadBearingClassifierCheckdescription rewrites "positionally match" to "equal as a multiset, in any order" and tightens the carrier guarantee. Pinned byInputFieldFkTargetNodeIdCase.FK_TARGET_REORDERED_KEY_PERMUTATION_DIRECT_FK{,_SINGULAR}over the newreordered_pk_parent/reordered_fk_childfixture (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 downstreamopptak-subgraphregelverksamlingIdschema. Follow-upfe2de55+beb0e92(@lookupKeycomposition guard): post-R131 the singular same-table@nodeIdcarrier isInputField.ColumnField(orCompositeColumnField), soEnumMappingResolver.buildLookupBindings’s pre-R131 structural rejection no longer fires; `beb0e92moves the guard upstream tosdlField.hasAppliedDirective(DIR_NODE_ID)so both arities surface the same diagnostic ("expose the decoded key column(s) explicitly via@fieldinstead, or move@lookupKeyto the outer argument"). NewLoadBearingClassifierCheckkeylookup-key-input-field-non-nodeid-decoded, new test casesGraphitronSchemaBuilderTest.ArgumentParsingCase.LOOKUP_KEY_ON_NODEID_INPUT_FIELD_REJECTED{,_COMPOSITE_PK}.NodeIdLeafResolver.validateLift/liftSourceColumnstightened 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@nodeIdpermutation pipeline test, to prove the multi-hop case the commit asserts works by construction) andR136(execution-tier round-trip for the FK-permutation case viagraphitron-sakila-example+GraphQLQueryTest). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25, 1900+ tests passing. -
R42 (
506c704+5d90719): LiftChildField.ColumnReferenceFieldout ofTypeFetcherGenerator.STUBBED_VARIANTSfor theCallSiteCompaction.Direct+ FK-only path. NewInlineColumnReferenceFieldEmitterbuilds the$fieldsswitch-arm body as a single-column correlated subquery (DSL.field(DSL.select(<terminalAlias>.<COL>).from(<terminalAlias>).join(...).where(<correlation>).limit(1)).as("<fieldName>")), mirroringInlineTableFieldEmitter’s shape collapsed to a scalar (DSL.field, not `DSL.multiset).TypeClassGenerator.$fieldsgains acase ChildField.ColumnReferenceField crfarm;TypeFetcherGeneratormoves the leaf fromSTUBBED_VARIANTSintoPROJECTED_LEAVESand the fetcher switch arm becomes a no-op.FetcherEmitterwiresnew ColumnFetcher<>(DSL.field("<name>"))for the Direct shape. The two non-lifted shapes surface at build time, not runtime:GraphitronSchemaValidator.validateColumnReferenceFieldrejectsNodeIdEncodeKeyswithRejection.Deferredkeyed tonodeidreferencefield-join-projection-form(R24) and anyJoinStep.ConditionJoin-in-path withRejection.Deferredkeyed to the newly-allocated R129 (column-reference-on-scalar-field-condition-join). The validator/emitter contract carries the@LoadBearingClassifierCheck/@DependsOnClassifierCheckannotation pair (keyscolumn-reference-field-no-nodeid-encode-keysandcolumn-reference-field-no-condition-join-step), soLoadBearingGuaranteeAuditTestwalks the dependency and the FetcherEmitterNodeIdEncodeKeysruntime stub forColumnReferenceFieldretires as defence-in-depth without an annotated guarantee (CompositeColumnReferenceField’s parallel arm is unaffected; R24 still owns it). Tests: `ColumnReferenceFieldValidationTestrestructured 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);NestingFieldValidationTeststubbed-nested cases switched toCompositeColumnReferenceField(which remains stubbed); new pipeline-tierColumnReferenceFieldPipelineTestcovers single-hop and multi-hop projection plusColumnFetcherwiring; sakila-example schema addsFilm.languageName: String @field(name: "NAME") @reference(path: [{key: "film_language_id_fkey"}]);GraphQLQueryTest.films_languageName_resolvesViaScalarReferencecovers the execution tier against PostgreSQL. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R43 (scoping change, this commit): The scalar/enum-return form of
@tableMethod(originally tracked as the carve-out undertablemethod-scalar-return.md) is closed by rejecting the shape at classification rather than implementing it.TableMethodDirectiveResolvernow rejects any non-TableBoundReturnTypereturn as a structural schema error ("@tableMethod requires a @table-annotated return type") at both root and child sites; the previous gating onisRootis gone, and the resolver’s sealedResolvedcollapses to{TableBound, Rejected}(theNonTableBoundarm was the only producer ofTableMethodFieldwith a non-table return and is dead code now).ChildField.TableMethodField.returnType()is tightened fromReturnTypeReftoReturnTypeRef.TableBoundReturnTypeto express the classifier guarantee in the model.TypeFetcherGenerator.STUBBED_VARIANTSkeeps theTableMethodFieldentry with reworded summary ("child @tableMethod (table-bound return) not yet implemented") and a renamed planSlugtablemethod-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.QueryTableMethodTableFieldat the root already ships inIMPLEMENTED_LEAVES). Pipeline tests inGraphitronSchemaBuilderTest.UnclassifiedFieldCasegain two cases (TABLEMETHOD_AT_ROOT_WITH_SCALAR_RETURN_REJECTED,TABLEMETHOD_ON_CHILD_WITH_SCALAR_RETURN_REJECTED) asserting the exact rejection message on both sites;TableMethodFieldValidationTestfixtures stop constructing the variant withScalarReturnType(no longer reachable from real classification) and useTestFixtures.tableBoundFilminstead. 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-dbon Java 25. -
R101 (
ef66e8bPhase 1;fbe354ePhase 2;46f08f8Phase 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 singleScalarTypeResolversource-of-truth carrying a sealedScalarResolution.{Resolved | Rejected}outcome. Consumers bind a custom scalar by pointing at apublic static final GraphQLScalarTypeconstant on the classpath, either by directive (scalar Money @scalarType(scalar: "com.example.Scalars.MONEY")) or by thegraphql-java-extended-scalarsconvention table (scalar BigDecimalresolves toExtendedScalars.GraphQLBigDecimalwhenever the artifact is on the consumer’s compile classpath, no directive needed). Graphitron reflects on the constant’sCoercing<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 frombuildSchema(...)hooks for any scalar graphitron now resolves; graphql-java’sGraphQLSchema.Builder.additionalTyperejects duplicate type names at build time, so leaving the call in turns into aSchemaProblemrather than silent tolerance. Resolution order: spec built-ins (Int,Float,String,Boolean,ID) win;@scalarTypebeats the convention layer; convention lands when the SDL name matches an entry onScalarTypeResolver’s 30-entry table and `graphql.scalars.ExtendedScalarsis on the classpath. Unresolved → hard validation error pointing at@scalarType(scalar:)or extended-scalars as the fix; no silent fallback toObject. 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; newBehavior.ScalarTypeBindingarm on the@scalarType(scalar:)coordinate;Documentation/code-generation-triggers.adocgains a@scalarTyperow; the scalar resolution story is documented in the manual reference page. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R127 (
8310179+6308198): AcceptList<XRecord>as well asResult<XRecord>at root@serviceon a@table-bound list return. The classifier’s strict catalog-sideTypeName.equalscheck now returnsnullfromServiceDirectiveResolver.computeExpectedServiceReturnTypefor theTableBoundReturnType+ List arm, and a new resolver-sidevalidateRootListTableBoundReturnPairrejects any reflected method-return that isn’t exactlyorg.jooq.Result<XRecord>orjava.util.List<XRecord>(graphql-java treats both identically;Result extends List).TypeFetcherGenerator.buildQueryServiceTableFetcherand.buildMutationServiceTableFetcherreadMethodRef.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 newLoadBearingClassifierCheckkeyservice-resolver-root-list-record-return-pairowns the resolver-side pair check; the existingservice-catalog-strict-service-returndescription narrows to the Single arm +ResultReturnTypepaths; both root emitters declare both keys via@DependsOnClassifierCheck. Tests.TestServiceStub.getFilmsAsListreturnsList<FilmRecord>;ServiceRootFetcherPipelineTestgains 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.LoadBearingGuaranteeAuditTestcovers the new key automatically. Workflow note. The item was filed directlyIn Progress: the inbound was framed as an operational bug report and the agent began implementation before the Backlog → Spec → Ready gate;CLAUDE.mdwas tightened in the same branch so the next operational-looking inbound (stack trace plus "make it accept X") doesn’t slip past. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25, 1564 tests passing. -
R68 (
f5c75ef; Phases 1a–6 SHAs compressed by upstream rebase): Diataxis user manual absorbs the legacygraphitron-codegen-parent/graphitron-java-codegen/README.mdinto the deployed site. Five top-level chapters under/docs/manual/(index,tutorial,how-to,reference,explanation) mirror the Diataxis quadrants. Tutorial (six pages anchored tographitron-sakila-example) verified byTutorialSmokeTest; a@QuarkusTestinside the example module replaying each page’s HTTP query against the JAX-RS endpoint. Reference: 26 directive pages 1:1 againstdirectives.graphqls(drift-pinned byDirectiveDocCoverageTest);mojo-configuration.adocreflected from the Mojo’s@Parameter-annotated fields (MojoDocCoverageTest);diagnostics-glossary.adoccovering the 16-codeRejectionKind/AttemptKind/EmitBlockReasonclosed set (DiagnosticsDocCoverageTest);deprecations.adocextracted from SDL@deprecated()markers with@indexallow-listed for the GraphQL-spec-disallowed whole-directive case (DeprecationsDocCoverageTest); plusruntime-api.adocandspecial-interfaces.adocas hand-curated prose. How-to: 14 recipe-shaped pages with "verified by" pointers into thegraphitron-sakila-example/src/test/java/.../querydb/consumer test surface (includingtest-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:15flipped from the legacy GitHub README pointer to the in-treexref:manual/reference/directives/index.adoc. Rework pass (f5c75ef): cleared fourR<n>leaks from user-facing prose flagged by the In Review reviewer ;R47reference inexternal-code.adoc:116dropped,R114inmulti-hop-nodeid-filter.adoc:15rephrased to feature-by-name, theuntil R61historical-Invariant bullet inresult-types.adoc:142dropped entirely, and theR75:Javadoc prefix onPassthroughDataFieldinChildField.java:361stripped at source (the migration fragment androadmap/inference-axis-coverage.adocregenerated). 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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 with 312 + 25 tests passing, all five drift-protection verifiers green. -
R44 (
b978e69): Deprecate@multitableReference, mirroring the@notGeneratedremoval end-to-end. The directive stays SDL-declared indirectives.graphqlsso consumer schemas still parse, butFieldBuilder.classifyFieldnow rejects every application with anUnclassifiedFieldcarrying aRejection.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 abovedetectChildFieldConflictso 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.MultitableReferenceFieldrecord and itspermitsentry deleted; sealed-switch exhaustiveness propagates the removal toGraphitronSchemaValidator(dispatch arm +validateMultitableReferenceFieldmethod gone) andTypeFetcherGenerator(STUBBED_VARIANTSentry + dispatch arm gone, with the[deferred] multitable-reference-on-scalarslug retiring as a dead anchor).detectChildFieldConflict’s mutual-exclusivity slot list drops `DIR_MULTITABLE_REFERENCEas dead vocabulary; theBuildContext.DIR_MULTITABLE_REFERENCEconstant, itsPASSTHROUGH_FORBIDDEN_DATA_FIELD_DIRECTIVESmembership, and theSchemaDirectiveRegistry.GENERATOR_ONLY_DIRECTIVESentry all retained per the deprecated-but-membership-retained precedent (the SDL declaration is still present). Tests. Pipeline-tierMultitableReferenceFieldCaserewritten from "produces aMultitableReferenceField`" to two cases: `REJECTEDassertsUnclassifiedFieldwith the deprecation reason, andREJECTED_WINS_OVER_CONFLICTpairs@multitableReferencewith@serviceand asserts the deprecation reason wins over the mutual-exclusivity reason (locking the ordering invariant). The redundantMULTITABLE_REFERENCE_AND_SERVICE_CONFLICTcase retires from the child-field conflict suite;MultitableReferenceFieldValidationTestdeleted outright. Docs.directives.graphqlsdescription rewritten in the@notGeneratedremoval shape;docs/manual/reference/directives/multitableReference.adocrewritten as a deprecation page (opener, SDL signature, Migration, Diagnostic, Constraints, See also); new=== @multitableReferencesubsection under "Hard removals" inmigrating-from-legacy.adoc; cross-references to@multitableReferenceretargeted to@discriminate/@discriminatorinreference.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 incode-generation-triggers.adocrewritten in the@notGeneratedrow’s shape. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R124 (
7882caf+f8d5300): Source the codegen reflection classpath from the project’s compile classpath + reactor siblingtarget/classes, not the plugin realm.AbstractRewriteMojo.withCodegenScopebuilds aURLClassLoaderoverproject.getCompileClasspathElements()plus the existingresolveClasspathRoots()set, parented on the plugin loader; the loader is threaded through a newRewriteContext.codegenLoaderfield to the 22 in-processClass.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 infinally, and closed to release JAR file descriptors (matters forDevMojo’s per-cycle regeneration). The lone `DataFetchingEnvironmentreflection inClassAccessorResolverstays plugin-internal. API surface:RewriteContextgains a non-nullcodegenLoaderfield (eight-arg compact + seven-arg + six-arg back-compat overloads default it to TCCL for unit-tier callers);BuildContext.codegenLoader()is a thin passthrough mirroringnodeIdLeafResolver()andBuildContext.ctxis now@NonNull-enforced viaObjects.requireNonNull(the three unit-tier tests that previously passed(null, _, null)now construct a deterministic stub via the 6-arg overload);JooqCatalogtakes a(String, ClassLoader)constructor with a one-arg TCCL-defaulting back-compat overload;CheckedExceptionMatcher.unmatched/coversandServiceCatalog.argExtractiongain aClassLoaderparameter;TypeBuilder.validateExceptionClassandFieldBuilder.checkDeclaredCheckedExceptionsflip fromstaticto instance (single same-class callers; the explicit-parameter sibling lives where it crosses a class boundary). Migration:<plugin><dependencies>blocks deleted fromgraphitron-sakila-example/pom.xmland thebasic-generateIT pom; the IT now declaresgraphitron-sakila-dbas a normal top-level<dependency>, locking the contract in the IT itself. Tests: new pipeline-tierCodegenLoaderTeststages a hand-rolled.classfile (Java 17 encoding inlined asbyte[], so the test does not need a compiler on its own classpath) under a faketarget/classesdirectory, wires its path throughproject.getCompileClasspathElements(), and asserts (a) the staged class is not on the test JVM’s classpath, (b) insidewithCodegenScopethectx.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 thegraphitron-sakila-examplereactor build, which now compiles and runs against the live schema with no<plugin><dependencies>block. Docs: new "Codegen classpath" section indocs/manual/reference/mojo-configuration.adocnames 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 toDevMojo.executeexplaining why the capturedinitialCtxmust 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 notstaticand why the cross-class siblings take an explicitClassLoaderparameter instead. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R83 (
b3c5b6c+1187fa5): Pipeline + compilation + execution tier coverage for the multi-schema jOOQ fixture R78 introduced. Three independent tiers ride themultischemafixturecatalog (multischema_a+multischema_b, the cross-schema FKgadget → widget, the collidingeventtable). Pipeline tier:MultiSchemaPipelineTest(annotated@PipelineTier, ingraphitron/src/test) drives a slim SDL throughGraphitronSchemaBuilderagainstjooqPackage=multischemafixtureand 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-schemaJoinStep.FkJoin.fk().keysClass()routes tomultischema_b.Keys(the FK-holder schema, not the target’s schema ; the R78 bug case);firstHop.targetTable().tableClass()lands onmultischema_a.tables.Widget. Structural emit-side: a typed walk overTypeSpec.methodSpecs[].parameters[].type/returnType/fieldSpecs[].typeplus parsed imports from the renderedJavaFile, asserting everyClassNamewhose canonical name starts with the multischema-fixture root lives under amultischema_a/multischema_bsub-package ; the bare-root R78 bug shape (<jooqPackage>.tables.X,<jooqPackage>.Keys,<jooqPackage>.Tables) cannot appear anywhere typed-reachable from aTypeSpec. Targeted positive assertions read parameter types directly offQueryConditionsGeneratorandTypeClassGeneratoroutput. Compilation tier: thirdrewrite-generate-multischemagraphitron-maven-pluginexecution ingraphitron-sakila-example/pom.xmlconsuming a newsrc/main/resources/graphql/multischema.graphqls(the same three shape cases as the pipeline test) and writing to a disjointno.sikt.graphitron.generated.multischemaoutput package. The fullmvn install -Plocal-dbreactor compiles the multischema slice against the live multi-schema jOOQ catalog; a regression that re-derives aClassNamefrom the barejooqPackageemits source that does not exist (root.Keys/root.tables.Widgetnever resolve under multi-schema codegen) and the maven-compiler-plugin fails the build. Execution tier:MultiSchemaQueryTest(annotated@ExecutionTier, ingraphitron-sakila-example/src/test) loads the multischema slice’s ownGraphitron.buildSchema(...)and issues three queries against therewrite_testPostgreSQL ; 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 toinit.sqlfor 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 originalb3c5b6crendered-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/@DependsOnClassifierCheckpattern 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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25, all 11 reactor modules SUCCESS, both new test classes pass (7 pipeline tests + 3 execution tests, plus the R78 unit-tierJooqCatalogMultiSchemaTest). -
R86 (
a53502f+ede87f1): Architecture chapter for the user-manual site. Newtyped-rejection.adocconsolidates the sealedResolvednarrative across the thirteen*DirectiveResolversiblings, theRejectiontaxonomy (AuthorError.{UnknownName | Structural | AccessorMismatch},InvalidSchema.{DirectiveConflict | Structural},Deferred), and theBuildContext.candidateHintLevenshtein contract; D10 draws theRejectionsealed hierarchy withLookupKeyDirectiveResolver.Resolved.{Ok, Rejected}overlaid as a worked example.README.adocrewritten as an intent-routed chapter index with D1 (build pipeline).getting-started.adoc § Federationand§ Dev loopeach 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). NewSealedHierarchyDocCoverageTestwalksRejection.permits()transitively with bidirectional drift protection ; alternation built from the live permit set so future top-level branches extend coverage automatically.ManualXrefIntegrityTestextended to remap renderedarchitecture/paths back tographitron-rewrite/docs/(mirrors thestage-architectureblock’sREADME.adoc → index.adocrename) so source-tree resolution stays honest without staging.rewrite-design-principles.adocframing line and the Builder-step-results-are-sealed section collapsed to forward pointers intotyped-rejection.adoc. Two acknowledged deviations: wire-format-encoding principle stayed inrewrite-design-principles.adoc(page name and consolidation list are about typed rejection, not wire-format decode at the DataFetcher boundary), and existing tables incode-generation-triggers.adocweren’t converted to enriched form (read fine as-is). Build green; all five*DocCoverageTestsiblings pass. -
R119 (
531495a+6a644f1+698a21a+22dfb8a+54fc7c9+4ae827d+04daf62+cc5417c): LSP completion / diagnostics keyed by GraphQL schema coordinates. SealedSchemaCoordinate(Directive/DirectiveArg/InputType/InputField) plus sealedBehavioroverlay (ClassNameBinding/MethodNameBinding(classNameCoord)/CatalogTableBinding/CatalogColumnBinding/CatalogFkBinding/ArgMappingBinding) live in a newLspVocabularyrecord that wraps a parsedTypeDefinitionRegistryof the bundleddirectives.graphqls.LspVocabularyconstructor enforces the structural invariant ; every overlay coordinate must resolve against the registry ; and throwsLspStartupExceptionotherwise;DriftDetectionTestbuilds the production overlay against the real SDL so R110-style drift is a startup failure, not a silent unknown-directive at request time. SingleRewriteSchemaLoader.directivesSdl()accessor consolidates what was two private constants. Seven consumers (Diagnostics,Hovers,ClassNameCompletions,MethodCompletions,FieldCompletions,TableCompletions,ReferenceCompletions) migrate to behavior-arm dispatch viaLspVocabulary.coordinateAt(directive, pos, source)+behaviorAt(coord); the@sourceRowgap 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) inDiagnostics, arg-name completion in a newArgNameCompletionsprovider chained last incoordinateBasedCompletions, and SDL-docstring fallback hover viaLspVocabulary.descriptionOf. Deletions:DirectiveDefinitions.java(124 lines),DeprecationMarkers.java(164 lines, two regex patterns),SdlAction.DeprecationTargetcollapses intoSchemaCoordinate.SdlActionDriftTestrewritten to read deprecations offLspVocabulary.deprecatedCoordinates()(native@deprecated(reason:)for member-level; docstring@deprecatedtoken-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@sourceRowregression-guard cases. Self-review (04daf62) liftedsiblingStringAttoLspVocabulary(~120 duplicated lines retire fromMethodCompletions/Hovers/Diagnostics) and filed R123 for theMethodNameBinding+METHOD_VALIDATING_DIRECTIVESenclosing-directive-context smell that surfaced when the consumers stabilised. Findings noted at approval (non-blocking): (1)Diagnostics.METHOD_VALIDATING_DIRECTIVESis the renamed shadow of the previousVALIDATE_METHODset; the spec said this would be replaced byMethodNameBindingarms only being attached where method validation applies, but the structural redesign got deferred to R123 instead of landing here. (2)SdlActions.detectLegacyNameSitescallsLspVocabulary.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 theSdlAction.Detectorsignature 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 unitBehaviorTestper-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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25, all 11 reactor modules SUCCESS, 187 LSP tests pass. -
Discarded: collapse
BatchKeyFieldvalidator/emitter redundancy (collapse-tabletargetfield-redundancy, R4): superseded wholesale by R58 Phase G (commit3dcd3c6), which introduced the orthogonalConditionJoinReportablecapability and collapsed the fourunsupportedReasonoverloads inSplitRowsMethodEmitterto a single capability dispatch + the validator’s matching 4-arminstanceofchain to a singleinstanceof ConditionJoinReportablecheck. 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-builtConditionJoinReportablerather than as a default onBatchKeyField, becauseServiceTableFieldis aBatchKeyFieldwithout 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
@splitQueryon@record-parent fields as a build warning (classification-vocabulary-followups, R3,17cc1a9+715a439):FieldBuilder.classifyChildFieldOnResultTypenow emits aBuildWarningviactx.addWarning(...)at both seams that head intoRecordTableField/RecordLookupTableField; the@sourceRowbranch (top of theif (DIR_SOURCE_ROW)block, beforeSourceRowDirectiveResolverruns) and the regular@record-parent branch (immediately afterresolveReturnTypeconfirmsReturnTypeRef.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-@recordprecedent atTypeBuilder.java:663; no new public API, no marker constant (deferred until R121’s LSP arm earns the second consumer). Closes the long-standing promise atcode-generation-triggers.adoc:105. Tests. Five pipeline-tier fixtures inGraphitronSchemaBuilderTest: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 assertsUnclassifiedField+ warning);SPLIT_QUERY_WARNS_ON_SOURCE_ROW,SPLIT_QUERY_WARNS_ALONGSIDE_SOURCE_ROW_REJECTION(@sourceRowpath; the last assertsUnclassifiedFieldwithRejectionKind.AUTHOR_ERROR+ warning). Each asserts the classification arm viaisInstanceOf(...)plus the warning’s message-substring onschema.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.aliasdead-storage cleanup (R120); generalising theBuildWarningchannel into aWarningKindenum (no fourth producer to justify the lift); rejecting@splitQueryon@record-parent fields (the directive remains classified-but-no-op so existing schemas keep building). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
Add
Record1<T>source-shape support alongsideRow1<T>on the@serviceclassifier path (emit-record1-keys-instead-of-row1, R61,742f11bd+3d01c218+4ffdfc02+8fc61b95+bfeae318): developers choose either source shape at the@servicesource declaration ;Set<Row1<Integer>>(Row surface, novalue1()) orSet<Record1<Integer>>(Record surface, withvalue1()) ; and variant identity tracks the chosen shape so emit sites pattern-match instead of re-deriving. The classifier already routedList<Row<N>>/Set<Row<N>>toRowKeyed/MappedRowKeyedandList<Record<N>>/Set<Record<N>>toRecordKeyed/MappedRecordKeyed; this iteration made each variant’skeyElementType()andjavaTypeName()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/LifterRowKeyedproduceRowN<...>;RecordKeyed/MappedRecordKeyed/AccessorKeyedSingle/AccessorKeyedManyproduceRecordN<...>.javaTypeName()per-variant routes to a sharedcontainerType(container, shape, cols)helper. Variant rename.AccessorRowKeyed{Single,Many}→AccessorKeyed{Single,Many}(8fc61b95): theRowdiscriminator 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/@externalFieldreturning aTableRecord; so the projection axis no longer encodes in the name. Used byServiceTableFieldandRecordTableField; the source-shape constraint lives in javadoc and is enforced byFieldBuilder.deriveBatchKeyFromTypedAccessor. Generators.GeneratorUtils.buildKeyExtraction(ParentKeyed) forks by variant identity:RowKeyed/MappedRowKeyedarms emitDSL.row(((Record) env.getSource()).get(table.col), ...);RecordKeyed/MappedRecordKeyedarms emit((Record) env.getSource()).into(table.col, ...).buildKeyExtractionWithNullCheckstaysRowKeyed-only with a defensive IAE on misroute (single-cardinality@splitQueryon a@tableparent is the only caller).buildFkRowKey(RecordParentBatchKey RowKeyed) reads scalar values per parentResultType(jOOQTableRecord, jOOQRecord, Java record getter, typed POJO getter) and constructs theRowN<...>viaDSL.row(...). NewbuildAccessorKeySingle/buildAccessorKeyManyemit__elt.into(table.col1, ...)to produceRecordN<...>keys, giving the auto-emitted rows-method’svalue<N>()access for the parent VALUES table emission. Parent VALUES emission.SplitRowsMethodEmitterforks two ways:RowN-keyed arms (RowKeyed,LifterRowKeyed) usek.field<N>()(returns the inline-valueFieldaDSL.row(value, ...)-constructedRowcarries);RecordN-keyed accessor arms useDSL.val(k.value<N>())(extract the scalar; wrap as a bind-parameterFieldthat typechecks against the inline-ifirst arg of jOOQ’sDSL.rowoverload). Without theDSL.valwrap the column-referenceFieldrendered into the VALUES table at runtime instead of the value. Lift Invariant #10 (bfeae318): the validator’s single-cardinalityRecordTableField/RecordLookupTableFieldrejection (validateRecordParentSingleCardinalityRejected) was a downstream gate stranded by the rows-method router pinning the single-record-per-key arm toAccessorKeyedManyonly. The data-fetcher side (buildRecordBasedDataFetcher) already handled single cardinality cleanly via the(dispatch == LOAD_MANY || !isList) -> RecordvalueType rule, so the only missing wiring was teachingemitsSingleRecordPerKey()to also be true for single-cardinality fields.RecordTableField.emitsSingleRecordPerKey()extends to!returnType().wrapper().isList() || batchKey() instanceof AccessorKeyedMany,RecordLookupTableFieldadds the missing override mirror, and the validator gate drops.@DependsOnClassifierCheckannotations. Two checks underbuildAccessorKeySingle/Manydescribing theField-typedinto(...)projection (accessor-rowkey-shape-resolvedfrom R60); paired with@LoadBearingClassifierCheckonFieldBuilder.deriveBatchKeyFromTypedAccessor. Tests. L1BatchKeyTestparameterised case pinskeyElementType()andjavaTypeName()per variant:RowKeyed,MappedRowKeyed,LifterRowKeyed→RowN<...>;RecordKeyed,MappedRecordKeyed,AccessorKeyedSingle,AccessorKeyedMany→RecordN<...>. L3ServiceFieldValidationTestadds dual-shape cells (MappedRowKeyed/MappedRecordKeyedboth classify cleanly on the same field). L3RecordTableFieldValidationTest/RecordLookupTableFieldValidationTestflip the threeSINGLE_*cases from rejection to acceptance. L3GraphitronSchemaBuilderTestaddsRECORD_TABLE_FIELD_SINGLE_CARDINALITYpinning post-R61 acceptance +emitsSingleRecordPerKey()projection. L4TypeFetcherGeneratorTest.serviceField_mappedRecord_list_keyTypeIsRecordNpins theSet<Record1<Integer>>parameter shape andrecord.into(...)extraction. L5TestServiceStub.javakeeps bothRow1-source andRecord1-source fixtures (getFilmsWithSetOfRow1Sources/getFilmsWithSetOfRecord1Sourcessiblings) as the dual-shape coverage anchor. L6FilmService.titleUppercase(Set<Record1<Integer>>) -> Map<Record1<Integer>, String>confirmsvalue1()works in the developer-side iteration; the existingRow1-source sibling confirmsfield1()-based dispatch keeps working. TheAccessorKeyedSingleexecution path is restored end-to-end against PostgreSQL (FilmCardData(FilmRecord film)+film: Film). Out of scope, deferred:@batchKeyLifterlifter return-type symmetry (the consumer-supplied static method still pinned toorg.jooq.Row1..Row22) ; owned by R71. Element-shape conversion forSet<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: "doesRow1afford a tuple-IN planner hint thatRecord1may not?" Resolution: no. jOOQ’sRecord1<T>extendsRow1<T>, so every typedRow1-API call site continues to type-check when handed aRecord1<T>; framework WHERE-clause emission reads keys viaRow-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) addedTableRecordKeyed/MappedTableRecordKeyedpermits to extend the cross-product to a third element shape (developer’s typedTableRecordsubtype);keyElementType()’s switch grew two cases, `buildKeyExtractiongrew a third arm emitting((Record) env.getSource()).into(Tables.X). R110 (75379091+3992f51+8922092+3b7f432) replaced@batchKeyLifterwith@sourceRowand splitLifterRowKeyedintoLifterLeafKeyed+LifterPathKeyedunder a newLifterKeyedsub-seal ofRecordParentBatchKey. R61’s row-vs-record symmetry survived: the lifter arms still produceRowN, the accessor arms still produceRecordN, 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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; current trunk passes the same test surface that landed at In Review. -
Replace
@batchKeyLifterwith@sourceRowcomposing with@reference(sourcerow-replaces-batchkeylifter, R110,7537909+3992f51+8922092+3b7f432):@batchKeyLifteris removed;@sourceRow(className, method)replaces it with flat args (noExternalCodeReferencewrapper, notargetColumns) and composes with@referenceso multi-hop paths from a non-table-backed@recordparent become expressible. Model.BatchKey.LifterRowKeyedsplits intoLifterLeafKeyed(JoinStep.LiftedHop hop, LifterRef lifter)(no-@referenceleaf-PK arm) andLifterPathKeyed(List<JoinStep> path, LifterRef lifter)(@reference-composed FK chain) under a newBatchKey.LifterKeyedsub-seal ofRecordParentBatchKey.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, andGeneratorUtils.buildRecordParentKeyExtraction’s exhaustive switch collapses both permits onto a single `case BatchKey.LifterKeyed lkarm because the lifter emit shape (Lifter.method((Backing) env.getSource())) is identical for both ; and what it doesn’t (theSplitRowsMethodEmitterprelude consumes both shapes via theJoinStep.WithTargetcapability andRecordParentBatchKey.preludeKeyColumns(), not aLifterKeyed-typed parameter, so the capability-uniformity claim is future-facing rather than load-bearing in production). Resolver. NewSourceRowDirectiveResolverwith sealed builder-internalDerivation.{Leaf | Path}typing the parent-side tuple source:Leafreads the leaf target’s PK columns directly (single column-equality JOIN);Pathdelegates toBuildContext.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>'").@referenceparse 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, andorg.jooq.Row1..Row22raw-return + arity-bounds checks before the per-position erasure loop. Three classifier-check keys (@LoadBearingClassifierCheckon the resolver, paired with@DependsOnClassifierCheckon consumers):sourcerow-classifies-as-record-table-field(resolver always projects intoRecordTableFieldorRecordLookupTableField, paired withSplitRowsMethodEmitter.emitParentInputAndFkChain);sourcerow-leafkey-batchkey-is-lifterleafkeyedandsourcerow-pathkey-batchkey-is-lifterpathkeyed(the no-reference vs reference-composed permit guarantee, both paired withGeneratorUtils.buildLifterRowKey). Two keys not one because the relaxation surface is independent: a future variant could allowLifterKeyedoutside the leaf-PK / path-keyed split without affecting the other guarantee.LoadBearingGuaranteeAuditTestpicks up all three pairs automatically. Tests. L1 unitBatchKeyTestaddslifterPathKeyed_emptyPath_throwsIllegalArgument(compact-constructor invariant) and extendsrecordParentBatchKeyExhaustiveSwitchCompilesAcrossPermitsto verify the sub-seal compiles to a singleLifterKeyedarm. L4 pipelineGraphitronSchemaBuilderTest.SourceRowClassificationCase(renamed fromBatchKeyLifterCase) carries 16 cases covering: pojo +Row1<Integer>+@referencehappy path; lookup-key co-presence; null-fqClassName /@table/ jOOQ-record parent rejects;JavaRecordTypeadmit; missing class / missing method / multiple matches / wrong return / wrong param type rejects; arity / column-class / wildcard mismatches;@referenceparse failure passthrough;@asConnectionreject;@field(name:)non-interaction; field-level@conditionco-presence;@orderByco-presence; scalar return reject;LEAF_PK_NO_REFERENCE(the new no-@referencearm producesLifterLeafKeyed);LEAF_PK_ARITY_MISMATCH(leaf-PK diagnostic distinguishes from path-keyed). All 1465 graphitron tests pass. Sakila fixtures.CreateFilmPayload.languagemigrated to leaf-PK (@sourceRowalone, no@reference). New Story 1 fixture:Query.customerAddressSummary(customerId: Int!): CustomerAddressSummarywhoseaddressfield carries@sourceRow + @reference(path: [{key: "customer_address_id_fkey"}]); the canonical path-keyed shape. Backed byCustomerAddressSummaryJava record,CustomerAddressSummaryLifter.addressIdOf(parent) -> Row1<Integer>, andCustomerAddressSummaryService. End-to-end coverage flows through the L5 compile-spec tier ongraphitron-sakila-example. Documentation. New how-todocs/manual/how-to/source-row.adoc(leaf-PK + path-keyed shapes with full SDL + Java + rejection-message anchors). Renamed referencedirectives/batchKeyLifter.adoc→directives/sourceRow.adocrewritten for the flat-args directive. Sweep acrossexternal-code.adoc(drops@sourceRowfrom theExternalCodeReferencetable; 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. Internalrewrite-design-principles.adocupdated for the sub-seal and renamed classifier keys. Architect-review revisions (3b7f432) tightened theLifterKeyedJavadoc to describe today vs. tomorrow without overclaiming, dropped uninstall AsciiDoc tag markers fromschema.graphqls(the docs build is plain Asciidoctor with no example resolver, sotag::sourcerow-leafpk[]/tag::sourcerow-story-1[]had no consumer), and filedR116(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-80still definesbatchKeyLifterwith the obsoletelifter: ExternalCodeReference+targetColumns: [String!]!shape (nosourceRowentry),.../diagnostics/Diagnostics.java:45lists"batchKeyLifter"not"sourceRow"inVALIDATE_METHOD, and three test files (DirectiveDefinitionsTest,DiagnosticsTest,ClassNameCompletionsTest) pin@batchKeyLifteras 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 fromdirectives.graphqlsso an IDE consumer will surface "unknown directive" diagnostics on@sourceRowand continue to suggest a removed@batchKeyLifter. The flat-args shape may require extendingDirectiveDef/InputTypeBindingto express non-ExternalCodeReferencearg 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(thelifter-methodentry) describes the wrong lifter shape: it says “methodName:” (the actual arg ismethod:) and “(Set<Key>) -> Map<Key, Value>” (that’s the@servicerows-method shape;@sourceRow’s lifter is `(parent backing class) -> RowN<...>per the resolver and the howto). Looks like the entry was copy-pasted fromattempt-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: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
Demote
@asConnection+ same-table@nodeIdguard from rejection to advisory warn (narrow-asconnection-same-table-nodeid-guard, R113,afedb4b8+846f055d+e5358818+7d59ffd5): R106 lifted same-table@nodeIdargs from aQueryLookupTableFieldlookup to aQueryTableField+BodyParam.Infilter 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 leafpredicate (pathRequired = outer arg && every nested input wrapper non-null) and collapsed the carrier into a sealedAsConnectionGuard.{None | Required(SameTableHit)}. Production schema (opptak-subgraph’sQuery.kompetanseregelverkGittIdV2(ider: [ID!]! @nodeId(typeName: "Kompetanseregelverk")): [Kompetanseregelverk!] @asConnection) deliberately composes that shape to ship a paginatedWHERE pk IN (decoded_ids)connection to consumers; the rejection blocked a wire format the producer authored on purpose. Rework demoted the rejection arm to aLOG.warnatFieldBuilder.resolveTableFieldComponents; classification falls through toQueryTableField+FieldWrapper.Connectionand the connection emitter ships the expected SQL. With the build break gone the sealed two-arm carrier collapsed further to a single nullableSameTableHit firstRequiredSameTableHitfield onNodeIdArgPlan(architect-review tightening ; sealed sub-taxonomy was justified to gate a rejection, not a single warn site). The warn routes throughASCONNECTION_HYGIENE_LOG = LoggerFactory.getLogger(FieldBuilder.class.getName() + ".asConnectionSameTableHygiene"), mirroring theBuildContext.idRefShimprecedent: stable category address for log filters and migration tooling, independent ofFieldBuilderclass organisation.formatAsConnectionSameTableRejectionrenamed toformatAsConnectionSameTableWarningwith 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-tierNodeIdPipelineTest.NodeIdConnectionAdvisoryCase(8 cases, all_ALLOWED; required arg/input field/conjunctive cases assertQueryTableField+FieldWrapper.Connection+BodyParam.Inon 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-tierAsConnectionSameTableWarnFormatTest(onerequiredLeaf_emitsWarn_namingFieldLeafAndTypecase via logbackListAppenderon the category logger; pins field/leaf/typeName +every page of @asConnection would equal the input setheadline +make 'ids' nullableadvisory hint ; the stable bits migration tooling can grep on). Execution-tierGraphQLQueryTest.filmsConnectionByRequiredIds_idsSupplied_paginatesBoundedSetmirrors the production shape (required outer wrapper on a same-table@nodeIdlist arg composed with@asConnection): three ids supplied withfirst: 2returns 2 withhasNextPage=true; page 2 after the cursor returns the remaining 1 withhasNextPage=false. Out of scope: directive-based warn suppression (three silencing routes already exist ; nullable leaf, drop@asConnection, FK-target arg);@LoadBearingClassifierCheckannotation (architect-review verified hygiene-only ; annotating would be inert); FK-target@nodeId+@asConnection(composes today viaResolved.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 atFieldBuilder.java:258referencesNodeIdArgPlan.AsConnectionGuard.Requiredand "rejection message" ; both removed by the carrier collapse. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
Multi-hop
@referencepath on@nodeIdfilter input fields, identity-carrying lift (multi-hop-nodeid-reference-filter, R114,b80594ff+0efba89f+0d12cc1):NodeIdLeafResolver.resolveFkJoinPathnow accepts@reference(path: [...])of length ≥ 2 on@nodeId(typeName: T)filter input fields and arguments when every step is aJoinStep.FkJoinand 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. TheDirectFkvsTranslatedFkdecision switches fromjoinPath.get(0)tojoinPath.getLast(); the resolver’sResolved.FkTarget.DirectFkgains aliftedSourceColumns: 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 atFieldBuilder.projectFiltersandFieldBuilder.walkInputFieldConditionsswap((JoinStep.FkJoin) joinPath().get(0)).sourceSideColumns()forliftedSourceColumns()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 amongA → ? → Cchains is the author’s responsibility via per-hop{ key: ... }. Two distinct@LoadBearingClassifierCheckkeys (not one widened key):nodeid-fk.direct-fk-keys-matchwidens to "the terminal hop’s target-side columns positionally match NodeType key columns"; newnodeid-fk.identity-carrying-liftcovers "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 onstatic final String LIFT_FAILURE_MARKER = "identity-carrying FKs"andCONDITION_STEP_MARKER = "must be a foreign key"constants onNodeIdLeafResolver; 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-tierNodeIdLeafResolverTest(3 new cases ;multiHopIdentityCarryingLift_succeedspins the lifted tuple shape on thelevel_a/b/cchain,multiHopLiftTranslationRejectedanchors onLIFT_FAILURE_MARKER,multiHopConditionStepRejectedanchors onCONDITION_STEP_MARKER); pipeline-tierNodeIdPipelineTest.{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-tierQueryConditionsPipelineTest.multiHopIdentityCarryingLift_emitsHelperOnLiftedTuplepins 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 onmvn install -Plocal-db’s `graphitron-sakila-examplecompile (the lifted-tuple type aligns withdecode<TypeName>helper signatures). Newnodeidfixturechain:level_a(PK(k1, k2)),level_b(PK(s, k1, k2), FK tolevel_aon(k1, k2)),level_c(PK(c, s, k1, k2), FK tolevel_bon(s, k1, k2)), pluslift_fail_{a,b,c}for the translation-failure case; both metadata-registered inNodeIdFixtureGenerator. Howto article atdocs/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 thenodeidfixturejOOQ classes live in a separate package from the sakila-example’sjooqPackage(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" viaExecuteListener) is deferred to a Backlog sibling because wiringnodeidfixture.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’sparent_node/child_reffixture. Out of scope, follow-ups filed: non-identity-carrying multi-hop@referenceon@nodeId(EXISTS-subquery / JOIN-with-translation emission, symmetric to R57’s single-hop translated FK case); renamecolumn/columnsslot 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.fkSourceColumnsvestigial slot (now fully covered byliftedSourceColumns). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25;LoadBearingGuaranteeAuditTestpicks 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 undergraphitron-maven-plugin/src/it/(basic-generate,missing-schema-inputs) failed inRewrite reactor CIbecause the forked child Maven could not resolveno.sikt:graphitron-sakila-db:10-SNAPSHOT; CI runsverify, which never installs sibling reactor modules into~/.m2, andinvoker:installonly 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 themaven-invoker-pluginconfiguration ingraphitron-maven-plugin/pom.xmlso the IT’s missing sibling rides the same reactor-cache resolutioninvoker:installalready uses, and rewrite the contract comment insrc/it/settings.xmlto name<extraArtifacts>as the seam for sibling-module IT deps so the next contributor adds an entry there rather than reintroducing an implicitmvn installprerequisite. 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 theRewrite reactor CIworkflow 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-moderuns both ITs to SUCCESS where they previously failed withCould not find artifact …graphitron-sakila-db:jar:10-SNAPSHOT. Out of scope: switching CI fromverifytoinstall(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-pluginis the only IT-housing module today). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
@record-parent multi-table polymorphic ChildField classifier arm (record-parent-multitable-polymorphic-classifier-arm, R105,76c3262f+518ffc70+c7c3579c+21af55604): theReturnTypeRef.PolymorphicReturnTypearm ofFieldBuilder.classifyChildFieldOnResultTypeis no longer a blanketRejection.deferred("@record type returning a polymorphic type is not yet supported", ""). Three of the fourBatchKey.RecordParentBatchKeypermits become reachable on@record-backed parents:RowKeyedwhen the parent is aJooqTableRecordType(hub = parent’s mapped table);AccessorKeyedManywhen the parent is aPojoResultType/JavaRecordTypeexposing a unique zero-argList<X> / Set<X>-returning accessor for some concreteX extends TableRecord(hub = accessor’s element-Record table).AccessorKeyedSingleis structurally derivable but deferred at the classifier (Rejection.deferred("polymorphic-child-record-parent-single-cardinality")):MultiTablePolymorphicEmitter.buildScalarPerParentFetcherreads parent context asRecord 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.LifterRowKeyedfor polymorphic returns stays deferred per Out of scope (@batchKeyLifter’s `targetTablederivation reads the field’s@tableelement type, which doesn’t apply to polymorphic returns). The hubTableRefis consumed at classification time (handed toresolveChildPolymorphicJoinPathsfor 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 resultPolymorphicRecordParentResolution.{Resolved(parentKey, hubTable) | Rejected(rejection)}per the principles' "Builder-step results are sealed" rule. New shared private helpercollectAccessorMatchesfactored out ofderiveBatchKeyFromTypedAccessor;deriveBatchKeyFromHubAccessoris 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). Themultitable-polymorphic-child.parent-key-extraction-is-batchkey-driven@LoadBearingClassifierChecksplits per-producer (…-table-backedonclassifyObjectReturnChildField,…-record-parentonclassifyChildFieldOnResultType) per the audit’s one-producer-per-key rule; the twoMultiTablePolymorphicEmitterconsumer call sites (buildBatchedConnectionFetcher,buildBatchedListFetcher) gain a second@DependsOnClassifierCheckfor the new key via the repeatable annotation. Newaccessor-rowkey-shape-resolved-against-hubkey onderiveBatchKeyFromHubAccessor: same-shape sibling of the existingaccessor-rowkey-shape-resolvedbut the identity contract is hub discovery rather than expected-table match.validateChildMultiTableParentPk(GraphitronSchemaValidator.java:347) drops theTableBackedType-gated early-return and readsfield.parentKey().preludeKeyColumns()uniformly across all fourRecordParentBatchKeypermits; signature losesparentTypeName, Map<String, GraphitronType> typesand 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-nullparentKeyandparentResultTypeviaObjects.requireNonNull, lifting the validator’s de-facto contract into the type system. Tests: pipeline-tierRecordParentMultiTablePolymorphicPipelineTest(new file undergraphitron/src/test/java/no/sikt/graphitron/rewrite/) drives the SDL → classifier path for all reachable permits ;childInterfaceField_recordParent_rowKeyedand_typeSpecEqualsTableBacked(parity-pin between the @record-JooqTableRecord producer and the table-backed producer viamethodSpec.toString()comparison so any drift across the two construction sites fails fast),_accessorKeyedMany(hub identity offLiftedHop.targetTable(),LOAD_MANYdispatch),_accessorKeyedSingle_deferred(DEFERRED rejection assertion for the Pojo + single-cardinality shape), andrecordParentPolymorphic_pojoWithoutMatchingAccessor_classifiesAsUnclassifiedField(three-option AUTHOR_ERROR with hub-author-error tail). UnionField siblings mirror the InterfaceField cases. Validator-tierInterfaceFieldValidationTestextends withrejects_listArm_onAccessorKeyedManyHubArityOver21(22-column hub PK onAccessorKeyedManytrips the same Row22 cap as the table-backedRowKeyedpath); the now-unreachablerejects_connection_onPkLessParentandrejects_listArm_onPkLessParenttests are dropped (empty-PK is unreachable through the canonical constructors). NewresultTypeFor(table)test-fixture helper publishes a sentinelJooqTableRecordTypefor the type-system non-null contract.GraphitronSchemaBuilderTest.NON_ERROR_POLYMORPHIC_FALLS_THROUGH_TO_DEFERRED_REJECTIONupdated: the Pet union fixture is Pojo-parent + single-cardinality, exactly the new deferred shape, and now lands onDEFERREDrather than the priorAUTHOR_ERROR. Out of scope, follow-ups: wideningMultiTablePolymorphicEmitter.buildScalarPerParentFetcherto consumeparentKey+parentResultTypeanalogously 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 beyondresolveChildPolymorphicJoinPaths; user-facing@record-with-polymorphic-children documentation. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
Lift same-table
@nodeIdarg/field to aWHERE pk IN (...)filter, not a lookup (nodeid-lookup-ignores-filter-siblings, R106,91c3cb892): same-typename@nodeIdargs on a table-bound query field now classify asQueryTableFieldwith aBodyParam.In/BodyParam.RowInpredicate against the table’s primary key, instead of the implicit promotion toQueryLookupTableField. Sibling filter args (scalar@condition/@field) compose with the@nodeIdarg as ordinaryBodyParampredicates rather than being silently dropped under the legacy lookup-promotion gate. Three classifier-seam edits inFieldBuilder.java: theResolved.SameTablearm readsarg.hasAppliedDirective(DIR_LOOKUP_KEY)instead of hard-codingisLookupKey = true(filter is the new default; explicit@lookupKeyre-enables the N×M derived-table shape); the blanket@nodeId @lookupKeyrejection 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 purelyhasLookupKeyAnywhere(fieldDef). Pulls the same-table@nodeIdpath onto the same filter rail as FK-target@nodeId(Resolved.FkTarget.DirectFkalready lifts toBodyParam.In/Eq/RowIn/RowEq), collapsing two near-identical paths into one and making mixed-shape inputs first-class. Tests: pipeline-tierNodeIdPipelineTest.ArgumentSameTableNodeIdCasemigrates 4 cases fromQueryLookupTableField/ScalarLookupArg/DecodedRecordtoQueryTableField/BodyParam.In/BodyParam.RowInassertions; newSAME_TABLE_WITH_FILTER_SIBLINGcase pins the headline composed-with-sibling lift on a composite-PK NodeType; newSAME_TABLE_WITH_EXPLICIT_LOOKUP_KEYcase pins that explicit@lookupKeyre-enables the lookup shape; newFK_TARGET_LOOKUP_KEY_REJECTEDcase pins the new FK-target rejection. Execution-tierGraphQLQueryTest.filmsByNodeIdArgWithTitleFilter_composesPkInWithSiblingFilterexercises the lift end-to-end (PK-IN composed withWHERE title = ?); the existingfilms_filteredByArgNodeId_returnsRowsMatchingDecodedIdstest stayed green with its comment refreshed for the new shape. No@LoadBearingClassifierCheckkeys touched (audit-inert flip). Out of scope, follow-ups: collapsingNodeIdArgPlan.{anyArgSameTable, anyNestedSameTable, sameTableHit}into a sealedAsConnectionGuard.{None | Hit}carrier (the@asConnectionrejection atFieldBuilder.java:403-407is the only remaining consumer; clean follow-up); the implicit@lookupKeydirective walkerinputTypeHasLookupKey; FK-target@nodeId(already a filter, no behavior change). Editorial follow-ups noted at approval (not blocking): stale comments atFieldBuilder.java:1105("the same-table arm synthesises isLookupKey: true") andBuildContext.java:181("same-table lookup vs FK-target filter") describe pre-R106 behavior and want a refresh. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
RC parity audit: classify GraphitronField/Type leaves and ship coverage gaps (
rc-parity-audit-leaf-coverage, R104, Phase 1a/1b14386cfc8, Phase 1a tests8ddfb272f, Phase 1c5bab9ca7f, Phase 1da032c96f3, Phase 2e0ff1f923, Phase 3c1fcbdcb2, Phase 4ba8149a7f, self-review fixesb947098e8, README linkacbcd4799, docs-site stagingadc63948a, rework32e10769): 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 `typesmap andGraphitronSchemaBuilder.buildSchema’s bare `fieldsmap becomeTypeRegistry/FieldRegistryprivate fields; type and output-field writes route throughclassify/enrich/demote/synthesize(each carrying a clean prior-entry precondition); the input-field path routes throughFieldRegistry.classifyInput(trace-only ; input fields are embedded in their parent type, not a central map, and the asymmetry is documented honestly rather than fought).ClassificationTraceis gated on-Dgraphitron.classification.trace=<path>; the new parent-pom-Pleaf-coverageprofile sets the property to${project.build.directory}/leaf-coverage.jsonlper module, with amaven-antruntruncate before the test phase so re-runs don’t append on top of stale records. A JUnit 5 extension auto-registered viaMETA-INF/servicesplusjunit.jupiter.extensions.autodetection.enabled=truetags every record produced inside a test’s lifecycle with the running test class and its tier annotation (resolved through the meta-@Tagon@UnitTier/@PipelineTier/@CompilationTier/@ExecutionTier, with@Tag("cross-cutting")exempted into a separate report column).roadmap-tool leaf-coverageopens an in-memory DuckDB connection, exposes the per-module JSONL files as a view viaread_json_auto('graphitron-rewrite/**/target/leaf-coverage.jsonl', union_by_name=true), stages parsedleaves(sealed permits + javadoc intent) andmentions(roadmap simple-name grep) tables, and renders both the internal report atgraphitron-rewrite/roadmap/inference-axis-coverage.adocand a consumer-facing--mode=migrationAsciiDoc fragment.directive-supportgains a sibling--mode=migrationrender. The migration guide atdocs/manual/how-to/migrating-from-legacy.adocinclude::`s both fragments under "Authoritative supported surface". Verify-mode of `roadmap-toolis bound to theverifyphase and fails CI when the README or the leaf-coverage report drift; the CI workflow now runsmvn verify -Plocal-db -Pleaf-coverageso the trace files exist for the verify check. Tests: unit-tierTypeRegistryTest(8) andFieldRegistryTest(5) pin precondition contracts;ClassificationTraceTest(5) documents the JSONL framing including ThreadLocal context inheritance and JSON escape;ClassificationTraceContextExtensionauto-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;DirectiveSupportReportTestextension covers the--mode=migrationrender. Honest deviations carried forward: thedirective-supportmigration fragment has no verify-mode CI binding in roadmap-tool’s verify phase because that fragment readsgraphitron-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 capturesLeafCoverageReport.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-coverageon Java 25. -
Extract
ConnectionPromoterfromGraphitronSchemaBuilder(extract-connection-promoter, R56,3f1c9af9+e94a1a9a): the ~250-line Connection-promotion concern (turning@asConnectioncarrier fields into proper Connection-typed fields, plus synthesising Connection / Edge / PageInfo entries onctx.typeRegistry) lifts into afinalpackage-private sibling undergraphitron/src/main/java/.../rewrite/.GraphitronSchemaBuilder.javashrinks 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 sealedResolved(the spec carved out why: this is a single-step structural transformation with no rejection arms; rejection of malformed@asConnectionusage already lives upstream inFieldBuilder.classifyField). The localbaseTypeName(GraphQLOutputType)helper migrates as a private static on the new class rather than being reconciled againstBuildContext.baseTypeName(GraphQLFieldDefinition)(different signatures, different unwrap semantics; consolidation is a separate decision if it ever matters). Tests: newConnectionPromoterTest(@UnitTier) exercises promotion directly via the existingGraphitronSchemaBuilder.buildContextForTestsseam (the R40 test-only entry point that runs the schema generator +TypeBuilderbut stops before field classification) ; eight focused-unit cases (directive-driven bare-list carrier, explicitconnectionName:, explicitdefaultFirstValue:, structural Connection-typed return enrich-path, SDL-declared@shareablePageInfoflag preservation, two-carrier dedup, return-type already names the Connection emits-no-rewrite, item-nullability propagation) plus anoSynthesisedTypesshort-circuit regression onrebuildAssembledForConnections. Existing pipeline-tier coverage (GraphitronSchemaBuilderTest@asConnectioncases,ConnectionRegistrationsTest,ConnectionTypeValidationTest) stays green as regression. TheDIR_AS_CONNECTIONdirective-presence assertion invalidateDirectiveSchemastays put (out of scope by spec). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
Batched key extraction for
ChildField.UnionField/ChildField.InterfaceFieldviaBatchKey(batch-multitable-polymorphic-child-fetcher, R102, Phases A-Df50f80b1, Phase Ed42eeebf, scalar-fetcher cleanupe1e45122, rework13eefedb, javadoc cleanup at approval): the multi-table polymorphic emitter no longer bypassesGeneratorUtils.buildRecordParentKeyExtraction; both arms (list and connection) readparentKey: BatchKey.RecordParentBatchKeyandparentResultType: GraphitronType.ResultTypeoff 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 withJOIN parentInputplus one per-typename SELECT, regardless of parent count);AddressOccupantsListBatchingTestpins the canonical sakila customer fanout to exactly 4 statements (down from ~14 pre-R102).BatchKeypermits with key-column components now enforce non-emptiness via compact canonical constructors (six direct + three viaJoinStep.LiftedHop’s slot-list invariant); `containerType’s dead empty-list fallbacks at `BatchKey.java:124, 133removed.validateChildConnectionParentPkrenamed tovalidateChildMultiTableParentPk, drops the Connection short-circuit, fires uniform N=21 cap on both arms (the sharedparentInput VALUESemitter widens toRow<N+1>, topping out at jOOQ’sRow22); the codegen-timeIllegalStateExceptionatMultiTablePolymorphicEmitter.java:682-693is removed in favor of the validator rejection. Newmultitable-polymorphic-child.parent-key-extraction-is-batchkey-driven@LoadBearingClassifierCheck↔@DependsOnClassifierCheckpair onFieldBuilder.classifyObjectReturnChildField(producer) and the two batched fetcher entry points (consumer).parentTableparameter dropped from everyMultiTablePolymorphicEmitterhelper signature; the dispatch site atTypeFetcherGenerator.java:436-461collapses accordingly. Tests: audit-tierBatchKeyTest(parameterised non-empty case per permit +JoinStep.LiftedHop); pipeline-tierTypeFetcherGeneratorTest(list-form DataLoader registration, key-tuple arity for single-PK and composite-PK parents, Interface/Union equivalence pin,buildRecordParentKeyExtractiondelegation pin);InterfaceFieldValidationTestandUnionFieldValidationTestmirror empty-PK and>21arity rejections plus 21-boundary well-formed cases on both list and connection arms; execution-tierAddressOccupantsListBatchingTestagainstAddress.occupants: [AddressOccupant!]!. Spec deviations carried forward: lifting the connection-rows participant single-PK truncation atMultiTablePolymorphicEmitter.java:824into the validator deferred (existingQuery.pagedItems → PagedA/PagedBwith composite(k1, k2)PK quietly works on the truncation; promoting it to a hard error would block landing). Thewrapperparameter for gating that check on Connection re-lands when the lift does. Out of scope, owned by R105:@record-parent classifier arm; lighting up theLifterRowKeyed/AccessorKeyedSingle/AccessorKeyedManypermits at classification time. R102 shipsRowKeyed-only; the slot type accepts the full four-permitRecordParentBatchKeysub-seal so R105 wires that arm in without re-touching the records or the emitter. Build green: graphitron module 1434/1434 on Java 25. -
@recordaccessor resolution validated at classify time (record-accessor-validation, R88,0bcb6ebe+b2d798a9+e5f2dd2e+863d90c5): downstream consumers gotcannot find symbol: getSakId()fromjavacon generated fetcher classes when the SDL field name didn’t match an accessor on the@record-backed POJO/Java-record (FetcherEmitter.propertyOrRecordValuesynthesised 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: newClassAccessorResolver.resolveruns reflective lookup atClass.forNameboundary (added to the reflection-roster atdocs/rewrite-design-principles.adoc:29), returning sealedAccessorResolution.{Resolved | Rejected}withResolveddirectly sealed overGetterPrefixed(Method) | BareName(Method) | FieldRead(Field). Resolution rules track graphql-java’sPropertyDataFetcherlookup 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 singleDataFetchingEnvironmentparameter or per-arg parameters whose types match the SDL arg list. Phase E’s type-system tightening narrows the slot onPropertyField.accessor/RecordField.accessorfromAccessorResolutiontoAccessorResolution.Resolved(still nullable for parents that don’t run reflective resolution: jOOQ-record-backed and null-fqClassNamePropertyDataFetcher-fallback parents);FieldBuilderroutesRejectedthroughUnclassifiedFieldcarrying a newRejection.AuthorError.AccessorMismatcharm whosemessage()appends the@field(name: "…")override hint inside the typed arm so discrimination rides on the seal rather than a message prefix;FetcherEmitterswitches exhaustively over the threeResolvedarms with no runtime fallback. Theclass-accessor-resolver-shape-guarantee@LoadBearingClassifierCheck↔@DependsOnClassifierCheckpair documents the contract; the consumer’sreliesOntext now describes a static type guarantee rather than a runtime invariant. Tests: pipeline-tierRecordFieldAccessorValidationTest(10 cases ; three rejection arms each pinning theUnclassifiedFieldrouting structurally + producing actionable validator diagnostics; six positive arms exercisingGetterPrefixed,BareName,FieldRead, full-env injection, per-arg injection, override-via-@field(name:)); execution-tierRecordExampleTypefixture (all three fields resolve toResolved, exercising the emit / execute boundary); audit-tierLoadBearingGuaranteeAuditTestpicks up the producer/consumer pair automatically. Implementation deviation: per-arg injection at the emitter usesMethod.getParameters()for argument names, requiring the backing class to be compiled with-parameters;methodCallExprthrows 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;PropertyDataFetcherfallback removal at the null-fqClassNamearm; Lombok / explicit-named accessor extensions; Levenshtein "did you mean" candidates. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbacross 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 → classNamemigration (lsp-externalcodereference-name-migration, R93, Phase 119e18b23, Phase 25258d16f, Cycle 275bee87b): two consumer-facing landings on the LSP. Phase 1 introducedDirectiveDefinitionsas the LSP’s directive-vocabulary registry (keyed on directive name; each entry carries(argName, inputType, nestedPath)tuples) and migratedClassNameCompletions.outerArgOf’s hardcoded three-directive lookup onto the derived view `argsByInputType("ExternalCodeReference"). The five sites that appeared indirectives.graphqlsbut were unwired in the LSP today (@externalField,@enum,@tableMethod,@batchKeyLifter, and the nestedReferenceElement.conditioninside@reference(path:)) gained completion + diagnostic surface as a side effect. Phase 2 added the code-action surface:SdlActionprimitive (namedDetector/Rewriteinterfaces, sealedRewriteResult.{Edit | Skip}so the bulk action’s count-by-reason pivot is typed) wrapping a single instantiation for thename → classNamemigration withtargets = { Member("ExternalCodeReference", "name") }; three activation points (per-site quick-fix on the cursor, file-scoped bulk action, workspace-scoped bulk action emitting a multi-documentWorkspaceEditdirectly with noexecuteCommandindirection); diagnostic stance splits on resolution: legacy-and-resolves stays silent (the build-channelLOG.warninFieldBuilder.parseExternalRefis the migration-tracking signal), legacy-and-unresolved fires error-severity diagnostic mirroring the build’sExternalRef.lookupErrorarm and naming the unresolved name plus the two fixes (namedReferencesconfig or writeclassName:directly). Bidirectional drift protection between SDL and theSdlActionsregistry: everySdlAction.targets()entry must resolve againstdirectives.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 anSdlActionor theMANUAL_MIGRATION_DEPRECATIONSallow-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 soDeprecationMarkerscan pick it up; no semantic change for consumers.CompletionDatagained anamedReferencesslot (4-arg canonical constructor; 3-arg secondary kept for test fixtures);Workspace.openUris()exposed for the workspace-scoped bulk action. Tests: unit-tierDirectiveDefinitionsTest,SdlActionTest(7 cases),DeprecationMarkersTest(10 cases including bundleddirectives.graphqlsparse),SdlActionDriftTest(4 cases including the at-landing-time canonical-set pin); LSP-tierCodeActionsTest(8 cases, including the cycle-2 sibling-diagnostic regression seam asserting the per-site quick-fix surfaces independently ofCodeActionContext.diagnostics),DiagnosticsTestextension (9 cases: one resolves-silent fixture, eight per-site unresolved-error fixtures one per ECR-binding directive, plus a canonical@servicemessage-content assertion naming the unresolved name and pointing at the two fixes),ClassNameCompletionsTestextension (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 nextSdlActionauthor):CodeActions.countableNounignores its parameter and hardcodes the R93 noun (fine for one action; brittle once a secondSdlActionlands);applyAll/countResolvable/countSkippedeach 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 unresolvedname:values (deferred until R90 Phase 3’s static-method index lands); renaming the@externalFielddirective itself (R54, disjoint SDL surface); automating consumernamedReferencesconfig 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 bindDSL.defaultValue(dataType)when the input omits the key (the column default lands; onNOT NULLcolumns 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 (explicitnullwrites SQL NULL via typed null binding). Update-side SET clauses are now built from a runtimeif (in.containsKey(name)) { sets.put(...) }walk overtia.setFields(): omitted columns drop out ofSETentirely, preserving the existing row’s value (PATCH semantics); explicit-null columns write SQL NULL. The UPSERT update branch shares the same dynamic SET walk overDSL.excluded(col), so an omitted column is no longer overwritten byEXCLUDED.colon conflict (which, paired with the insert-sideDEFAULTcell, 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 explicitnullin the input map; graphql-java’s argument coercion preserves the absent-vs-null distinction in the resultingMap<String, Object>(Map.containsKey is the dispatch key). The structural pin lives inFetcherPipelineTest.dmlInsertField_*containsKey*/dmlSingleRowUpdateField_emitsDynamicSetWalkOverInKeySet; execution-tier coverage inGraphQLQueryTest.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-armvalueTypelift, 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_raisesErrorinGraphQLQueryTest); Phase G (37622aa75) scrubbed eleven stalemutations.mdcitations acrossDmlReturnExpression,FieldBuilder,TypeFetcherGenerator,GraphitronSchemaBuilderTest, andTypeFetcherGeneratorTest. Spec deviation acknowledged at approval: theupsertFilms_doNothingMode_skipsUniformityGuardexecution test routed to the pipeline tier (FetcherPipelineTest.dmlUpsertField_bulkInput_doNothingMode_omitsUniformShapeAndSetMapEmits) because PostgreSQL enforces NOT-NULL before evaluatingON CONFLICT, so the doNothing-with-divergent-shapes SQL can’t be exercised against Sakila’sfilmtable; the pipeline-tier substitute pins the structural claim (nofirstKeyscapture, nosetsUpdatewalk, no.doUpdate()clause,.onConflict(…).doNothing()chain present, bulkList<Map<?,?>>cast preserved). Closing landing: In Progress → In Review ate9746c7ea; In Review → Done approved on this commit. -
Path expressions in
argMapping(argmapping-path-expressions, R84,b3f85cd–91dd082): the right-hand side of anargMappingentry on@service/@tableMethod/@conditionmay now be a dot-path that walks into nested input fields (e.g.kvotesporsmal: input.kvotesporsmalId). New sealedPathExpr.{Head | Step}carrier replacesMap<String, String>onArgBindingMap.byJavaName;ArgBindingMap.of(slotTypes, segmentChains)walks segment chains against the GraphQL schema and populates a per-stepliftsListflag so the emitter never re-asks the schema. NewResult.PathRejectedarm covers structural rejections (walk-through scalar/enum/union/interface; unknown segment with closest-match hint). Multi-segment flat paths route through the existingCallSiteExtraction.NestedInputFieldmachinery; intermediate-list paths route through a newArgCallEmitter.buildListAwarePathExtractionwalker that emits element-wise.stream().map(...).toList()for eachliftsList=truesegment (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.reflectServiceMethodparameter-mismatch hint mentions path expressions on every rejection that prints anargMappingexample, 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 fromServiceDirectiveResolver). Spec deviations (acknowledged at approval time):Result.PathRejectedreplaces the spec’spathErrorcarrier slot since path resolution lives inArgBindingMap.ofand a separate slot would be redundant metadata; the parallel walker inArgCallEmitterwas preferred over augmentingNestedInputFieldwith per-segmentliftsListto avoid threading an always-falseflag 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);ServiceCatalogTestPhase F cases (floor mention, no-args negative, stretch unambiguous-prefilled, ambiguous-fallback, type-mismatch-fallback, 6-arg-overload-fallback); execution-tierGraphQLQueryTestsakila fixturesfilmsByPath(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 returnsTableResolution.{Resolved | NotInCatalog | Ambiguous(schemas)}andfindForeignKeyByName(String)returnsForeignKeyResolution.{Resolved | NotInCatalog};BuildContext.synthesizeFkJoinreturnsFkJoinResolution.{Resolved | UnknownTable(name, failure) | UnknownForeignKey(fkName)}so the fourOptional<TableRef>and fourOptional<FkJoin>rejection sites switch on variant directly instead of fabricating eight distinct "not in catalog" strings.JoinStep.FkJoin.fkis non-null by canonical-constructor enforcement; the redundantString fkNamecomponent drops (carriers readfk.sqlName()). Catalog construction asserts every schema in the live jOOQ catalog publishes a generatedTablesclass, throwingIllegalStateExceptionpointing at<tables>true</tables>on miss;TableEntry.toTableRefis consequently non-Optional.findCandidateSchemasFordeletes (theAmbiguousarm carries the schema list inline);findUnqualifiedTabledrops.limit(2)sinceAmbiguousneeds the full list. Diagnostic-builder consolidation:BuildContext.unknownTableRejectionswitches onTableResolution(Ambiguous → structural with qualified-form suggestions; NotInCatalog → unknownTable with Levenshtein candidates); new siblingunknownForeignKeyRejectioncovers FK-name misses.ServiceDirectiveResolver.computeExpectedServiceReturnTypejavadoc drive-by:<jooqPackage>→<schemaPackage>(post-R78 phrasing). 26new FkJoin(name, null, …)test fixtures migrate toTestFixtures.foreignKeyRef(...)factory. Tests:JooqCatalogMultiSchemaTestextends to 45 cases ; threeTableResolutionarms by name on the multischema fixture (widgetresolved,eventambiguous in both schemas, fabricated namesNotInCatalog); bothForeignKeyResolutionarms; all threeFkJoinResolutionarms viasynthesizeFkJoin; the construction precondition (staticverifyTablesClassPresenthelper); both diagnostic builders assertingRejection.AuthorError.UnknownNameshape andAttemptKind.FOREIGN_KEYtagging. TheFkJoinResolution.UnknownForeignKeyarm is structurally unreachable from current production callers (they pre-resolve the FK viafindForeignKeybefore callingsynthesizeFkJoin); 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.synthesizeFkJoinpaired slot[i] positionally fromForeignKey.getFields()andForeignKey.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 withPRIMARY KEY (pk_a, pk_b, pk_c), jOOQ’sgetKey().getFields()returns(pk_a, pk_b, pk_c)whilegetKeyFields()returns(pk_b, pk_c, pk_a). ZippinggetFields()against the former produced silent mis-paired slots ; observable asField<Long>.eq(Field<String>)compile errors in generated@splitQueryrows-method JOIN ON predicates downstream when the FK column types are heterogeneous. The fix swapsf.getKey().getFields()forf.getKeyFields()(the FK’s own ordered referenced-column list, parallel togetFields()by jOOQ’s contract) atBuildContext.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 successorJoinSlotOrientationTestconstructs slots directly viaTestFixtures.fkJoinrather than driving them throughsynthesizeFkJoinagainst 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 atgraphitron-sakila-examplewas silent too. Reproducer fixture:nodeidfixture.reordered_pk_parent(PRIMARY KEY (pk_a bigint, pk_b varchar, pk_c varchar)) plusnodeidfixture.reordered_fk_childwhoseCONSTRAINT 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 inColumnRef.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: newSynthesizeFkJoinReorderedKeysTest(graphitron/src/test/java/no/sikt/graphitron/rewrite/) drivessynthesizeFkJoinagainst 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()andtargetSideColumns()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-dbSUCCESS 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):JoinSlotsealed interface (FkSlotpairs source/target columns;LifterSlotcollapses both onto a single column by construction so DataLoader-key-tuple-IS-target-column-tuple is a type fact, not a prose precondition).JoinStep.FkJoinandJoinStep.LiftedHopcarryList<JoinSlot>; theWithTargetcapability returnsIterable<? extends JoinSlot>fromslots()so positional access (.get(i),.getFirst(),.subList(...)) is a compile error at every consumer.BuildContext.synthesizeFkJoinorients 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.matchingParticipantColretired whole;JoinPathEmitter.emitCorrelationWheredrops itsparentHoldsFkparameter and arity-mismatch throw;InlineTableFieldEmitter,InlineLookupTableFieldEmitter,TypeFetcherGeneratorretire theirparentHoldsFkderivations off cardinality / target-table;SplitRowsMethodEmitter.buildSingleMethod’s if/instanceof FkJoin/LiftedHop block collapses to a single `firstHop.slots()iteration throughWithTarget. Producer@LoadBearingClassifierCheckonWithTarget.sourceSideColumns()/ keyfk-join.slots-oriented-source-and-targetpaired with@DependsOnClassifierCheckon every migrated reader (FieldBuilder ×3, NodeIdLeafResolver, JoinPathEmitter, TypeFetcherGenerator, MultiTablePolymorphicEmitter ×2). Self-ref deviation from spec: spec promised "no signature change, no caller-supplied hint" onsynthesizeFkJoin; the table-name comparison is ambiguous for self-referential FKs (category.parentvscategory.childrennavigate the same FK in opposite directions). The fix threads aselfRefFkOnSourceboolean (derived from list-cardinality at the call site) throughparsePath→parsePathElement→synthesizeFkJoin, consulted only in the self-ref case; non-self-ref FKs ignore it. Tests:JoinSlotOrientationTest(5 model-tier tests pinning slot orientation + theIterable<? extends JoinSlot>compile-time ban);TestFixtures.fkJoin/liftedHophelpers convert nine test fixtures from positional pairs to slot pairs; the two body-string regressions added atfdfec353(splitTableField_listRowsMethod_reorderedHeteroFk_pairsBySqlNameAndType,childInterfaceField_connection_reorderedCompositeFk_pairsBySqlNameAndType) retire in favour of the structural model-tier coverage plus the existing compile-tier check atgraphitron-sakila-example; two execution-tier tests (inlineTableField_selfRef_listCardinality_returnsChildren,inlineTableField_selfRef_nonRootCategory_hasNoChildren) caught the self-ref ambiguity.rewrite-design-principles.adoc:228DTO-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 `parentPkColsparameter is dead after the lift (the author flagged this in the Javadoc). The self-refselfRefFkOnSourcethreading is a design fork worth revisiting: a post-synthesis slot-orientation hint onJoinStep.FkJoinset 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 typedRow<N><T1, ..., TN>(orList<Row<N><...>>) instead of erasing toRowN, applying the typed adapter / composer pairs principle (added inaa66c7e).BodyParam.RowEq/RowIndrop the deadjavaTypeslot;TypeConditionsGenerator.rowTypeNamebuilds the parameter type fromColumnRef.columnClass(), replacingDSL.row(new Field<?>[]{...})with the typedDSL.row(table.c1, ..., table.cN)form.ArgCallEmitter’s inline arity > 1 path uses a Java-17-compatible raw-RecordN` pattern + cast to the typedRow<N><...>(parameterizedinstanceofpatterns are JDK 21+); the cast is unchecked at the type-arg level but sound at runtime since the decoder returnsRecord<N><T1, ..., TN>. Arity > 22 is rejected upstream as a deferredRejection.structuralinNodeIdLeafResolver.resolvewith wording tracking thevalidateChildConnectionParentPkRow22 precedent. TheQueryConditionsGeneratorshim layer additionally (a) hoists per-class composite-key NodeId decode chains into private static helpers via aCompositeDecodeHelperRegistrydeduplicated by(encoderClass, methodName, mode, list)with namingdecode<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 thenoCondition()-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-tierQueryConditionsPipelineTest(helper-dedup + scalar/list key separation), unitCompositeDecodeHelperRegistryTest(same-key dedup, SKIP↔THROW separation, scalar↔list separation, per-mode body shape ; six tests), unitQueryConditionsGeneratorLiftTest(≥2-share lifts, single does not, distinct outer args do not, cross-filter counts, camelCase naming ; five tests),NodeIdLeafResolverTest.rejects_whenNodeTypeKeyArityExceeds22against a synthetic 23-column PK innodeidfixture, and a sakila compilation-tier regression-guard fixture (filmActorsByCompositeNodeIds+FilmActorCompositeNodeIdFilter) exercising theBodyParam.RowIn→ typedRow<N>path against real jOOQ.TypeConditionsGeneratorTest.nodeIdInFilter_compositeColumns_emitsRowInWithUntypedRowNrenamed and rewritten to assert on the typed form plus aList<Row2<Integer, Integer>>parameter-type sibling assertion. -
Replace string-scan helper-emission gate with
TypeFetcherEmissionContext(type-fetcher-helper-emission-gate, R80,c36734d+c5506e1):TypeFetcherGenerator.generateTypeSpecpreviously decided whether to emit thegraphitronContexthelper by serialising every just-emitted method’sCodeBlockand substring-greping forgraphitronContext(env); an enumerate-or-scan gate whose latest near-miss had silently droppedServiceRecordField(the onlyBatchKeyFieldthat doesn’t extendSqlGeneratingFieldviaTableTargetField). Replaced with a per-classTypeFetcherEmissionContextscratchpad: every emitter that writes agraphitronContext(env)call obtains theCodeBlockthroughctx.graphitronContextCall(), which records the dependency on the way out; class assembly drainsctx.isRequested(GRAPHITRON_CONTEXT)and emits the helper accordingly. ThreadedctxthroughArgCallEmitter(bothbuildCallArgsoverloads, bothbuildMethodBackedCallArgsoverloads,buildArgExtraction),LookupValuesJoinEmitter.buildFetcherBody,SplitRowsMethodEmitter(entry points +emitParentInputAndFkChain),MultiTablePolymorphicEmitter(emitMethods/emitConnectionMethodsoverloads + the four privatebuild*statics), and the in-fileTypeFetcherGenerator.build*privates. Replaced 11 SQL-context literals (graphitronContext(env).getDslContext(env)), the validator pre-step’sgetValidator(env), and the multitable tenant-id data-loader name composition with$Linterpolation ofctx.graphitronContextCall(). Test impact:graphitronContextHelper_emittedForServiceRecordOnlyClasskeeps 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 throwawayctxconstructed 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 commitc5506e1corrects the comment; R85 (helper-emission-non-fetcher-hosts) generalisesEmissionContextto 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 fullmvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R68 Phase 1b: tutorial chapter +
TutorialSmokeTest(diataxis-user-manual, R68,fa36dbc+d0c63c4): six tutorial pages underdocs/manual/tutorial/plus a@QuarkusTestdrift verifier ingraphitron-sakila-example. Prose (fa36dbc):01-prerequisites.adoc(JDK 25, Maven, Docker,git; thedocker 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 typesQuery/Customer/Addressand how@table+@fieldmap them to PostgreSQL; honest call-out on@nodeId/@nodebeing 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; thecustomers(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-hopstoreAddresschain throughcustomer.store_id → store.store_id → store.address_id → address.address_id; the inferred-FK shape onStore.customers);05-mutations.adoc(theFilmCreateInput @table(name: "film")+@mutation(typeName: INSERT)shape; theRETURNINGclause keeping the round-trip count to one;UPDATEvia@lookupKeyonfilmId);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.adocreplaced 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 usesaddress { address district }instead ofaddress { addressLine1 }because the example schema doesn’t carryaddressLine1; (2) the "going further" page links toconnections+sort-resultsrather than the plan’s earlierpagination-and-sortingbecause Phase 3 split that recipe into the two shipped pages. Smoke test (d0c63c4):TutorialSmokeTestlives next to the existingGraphqlResourceSmokeTestundergraphitron-sakila-example, reusing theSmokeTestPostgresResourceQuarkusTestResourceLifecycleManager(Testcontainers Postgres orlocal-dbrouting 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 threeactivebool: truerows),page4_singleHopReference(theaddress { address district }shape with47 MySakila Drive),page4_multiHopReference(thestoreAddresschain returning both store addresses),page5_createAndUpdateFilm(POSTcreateFilmround-trips afilmId > 5; subsequentupdateFilmagainst that id round-trips the renamed title).@AfterEachDELETE FROM film WHERE film_id > 5keepsApprovalQueryExampleTest’s five-film pin honest. Plus a "Use GraphiQL instead of curl" subsection on `01-prerequisites.adoc(the bundled playground athttp://localhost:8080/graphiql/) and a one-line nudge in03-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-testMaven module wrapping a shell script aroundmvn quarkus:devandcurl. The shipped shape is a@QuarkusTestinside the existinggraphitron-sakila-exampletest source. The in-module shape exercises the same JAX-RS endpoint and the sameGraphqlEngine/GraphqlResource/AppContextbean wiring thatmvn quarkus:devwould, runs naturally inside themvn verify -Plocal-dbinvocation 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 ashipped at fa36dbc + d0c63c4trailer; 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-db250/250 (244 prior + 6 new);ApprovalQueryExampleTestcontinues 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:15flipped from the legacygraphitron-codegen-parent/graphitron-java-codegen/README.mdGitHub pointer to the in-treexref:manual/reference/directives/index.adocso readers land on the live, drift-protected directive reference Phase 2 shipped undermanual/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), andhow-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 863d8betrailer; 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 underdocs/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 tographitron-principles.adoc) andwhy-jooq-and-graphql-java.adoc(jOOQ is where you work, GraphQL-Java is under the hood, the dependency triple constrains and enables; cross-links todependencies.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 aCustomerschema showing same-scope join,@splitQuery-driven batch, and@externalFieldcomputed 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;@splitQueryopens a new scope keyed by parent PK;@lookupKeyparameterises a derived target table; the N × M contract that custom@conditionmethods must respect; the per-requestDataLoaderRegistrylifecycle). "Why does it work that way" answers:design-decisions.adoccovers seven user-visible constraints with their rationale (why@conditionmethods take a table parameter even when not aliased; why@lookupKeyblocks pagination; why mutations require@tableon the input type rather than reusing the output’s binding; why selection drives projection; why federation_entitiesis a@lookupKeyshape; 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 at868593a(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 underdocs/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 tographitron-principles.adoc) andwhy-jooq-and-graphql-java.adoc(jOOQ is where you work, GraphQL-Java is under the hood, the dependency triple constrains and enables; cross-links todependencies.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 aCustomerschema showing same-scope join,@splitQuery-driven batch, and@externalFieldcomputed 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;@splitQueryopens a new scope keyed by parent PK;@lookupKeyparameterises a derived target table; the N × M contract that custom@conditionmethods must respect; the per-requestDataLoaderRegistrylifecycle). "Why does it work that way" answers:design-decisions.adoccovers seven user-visible constraints with their rationale (why@conditionmethods take a table parameter even when not aliased; why@lookupKeyblocks pagination; why mutations require@tableon the input type rather than reusing the output’s binding; why selection drives projection; why federation_entitiesis a@lookupKeyshape; 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 at868593a(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 pagereference/deprecations.adoclists every SDL@deprecated()marker indirectives.graphqls(currently@asConnection(connectionName:)andExternalCodeReference.name) plus the one whole-directive deprecation that the GraphQL spec disallows@deprecatedon (@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@notGeneratedso 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@Deprecatedannotations 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:DeprecationsDocCoverageTestextracts qualified<parent>.<member>keys from the SDL by walking backwards from each@deprecatedhit to the closestdirective @<name>orinput <Name>declaration, then asserts every key’s two halves both appear in the doc page (rows naturally include both, e.g.@asConnection(connectionName:)andExternalCodeReference.name). Whole-directive deprecations are covered via a smallWHOLE_DIRECTIVE_DEPRECATIONSallow-list (currentlySet.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 ofDirectiveDocCoverageTest. 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 at868593a(runtime-api) andd796c4c(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
@tablerejection: 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. NewBuildContext.unknownTableRejection(String sqlName)helper consolidates the@table(name:)rejection-construction decision: branches onJooqCatalog.findCandidateSchemasFor(size >= 2emits a structural ambiguity message naming the colliding schemas plus inline qualified-form suggestions; otherwise falls through to the existingRejection.unknownTablewith 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 inTypeBuilder(buildTableType,buildTableInterfaceType,buildTableInputType) all route through it, so the better message reaches authors at every directive site that resolves aname:argument. The eight non-@table-directiveresolveTablecallsites (one inNodeIdLeafResolver, four inBuildContextresolving 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.adocgrows a sentence undername:describing the qualified form and a constraint bullet quoting the new rejection verbatim;how-to/map-types-to-tables.adocgrows 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 inJooqCatalogMultiSchemaTestpin the three branches against the multischema_a/multischema_b fixture (ambiguous unqualified, missing unqualified, qualified miss); construction passesnullschema andnullctx since the helper only touches the catalog andBuildContext.buildTypeNamesByTableKeyalready 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.findTablepicked whichever schema iterated first on a name collision) by replacing string concatenation againstString jooqPackagewith javapoetClassNamevalues populated once at parse time fromTable<?>reflection. Model:TableRefcarriestableClass(the<schemaPackage>.tables.<X>class),recordClass(the<X>Recordclass viaTable.getRecordType()), andconstantsClass(the schema’sTablesclass) asClassName; the priorString javaClassNamefield is gone. NewForeignKeyRef(sqlName, keysClass, constantName)replacesString fkJavaConstantonJoinStep.FkJoin; theKeyshost class is the FK-holder schema’s so cross-schema FKs join correctly without per-emitter schema arithmetic. Catalog API:JooqCatalog.TableEntryexposestableClass(),recordClass(),constantsClass()typed accessors plustoTableRef(sqlName)as the single factory, collapsingBuildContext.resolveTableandServiceCatalog.buildTableRef(which already returnedOptional<TableRef>);findForeignKeyByName(sqlConstraintName): Optional<ForeignKeyRef>replacesfkJavaConstantName(): Optional<String>. Catalog-miss is structural:BuildContext.resolveTablereturnsOptional<TableRef>andsynthesizeFkJoinreturnsOptional<FkJoin>; consumers route absence throughUnclassifiedType/UnclassifiedFieldrather than empty-string sentinels, so emit sites never see a partial ref. Resolution-time disambiguation: newparseQualifiedTableName(String)plus twofindTableshapes (findTable(qualifiedName)andfindTable(schemaSqlName, tableSqlName)); unqualified@table(name: "x")resolves iff exactly one schema containsx, 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 inBuildContext, four inTypeBuilder, one inNodeIdLeafResolver) all participate; directive SDL signature is unchanged.jooqPackagesurvivors: onlyJooqCatalog’s constructor (loads `<jooqPackage>.DefaultCatalogreflectively) andCatalogBuilder’s filesystem-path conversion (replace('.', '/')); no emitter takes a `jooqPackageparameter, no concrete+ ".tables"/+ ".tables.records"/ClassName.get(jooqPackage, "Tables" | "Keys")concatenation survives anywhere ingraphitron/src/main/.NodeIdEncoderClassGeneratorreadsnt.table().constantsClass()perNodeTypeinstead of synthesising one fromjooqPackage;QueryNodeFetcherClassGenerator.generatedrops a deadjooqPackageparameter;EntityFetcherDispatchClassGeneratorandSelectMethodBodyswitch toentity.table().tableClass();GeneratorUtils.ResolvedTableNamescollapses to a singletypeClassfield with the other two reading fromTableRef; the privatebuildRowKey,buildAccessorKey{Single,Many},buildKeyExtraction{,WithNullCheck}helpers become parameter-pure onjooqPackage. Test surface: newmultischema_a+multischema_bjOOQ-codegen fixture ingraphitron-sakila-dbwith cross-schema FK (gadget → widget), a sharedeventtable in both schemas, and a uniquewidget/gadgetper schema;JooqCatalogMultiSchemaTestasserts cross-schematableClass/recordClassFQNs (multischema_a.tables.Widget,multischema_a.tables.records.WidgetRecord), the FK-holderKeysclass for cross-schema traversal (multischema_b.Keys), schema-qualified resolution, unqualified-and-unique resolution, and the empty-on-ambiguity policy for the sharedeventname;TestFixtures.tableRef(...)helper centralises the newClassNameconstruction across the 107 test sites. Phasing: Phase 1 fixture landed first to turn the compilation tier red; Phase 2 (catalog API +TableRefmigration + 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 deadjooqPackageparameter 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 additionsfor next-touch-oftable.adocandmap-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-dbSUCCESS on Java 25. -
TableRecord-keyed Map returns on
@servicerows methods (service-rows-tablerecord-key-shape, R70): closes R32’s deferred "element-shape conversion when the developer’sSourcesisSet<TableRecord>/List<TableRecord>`" bullet by extending the variant taxonomy rather than threading conversion through the emitter. Two new `BatchKey.ParentKeyedpermits ;TableRecordKeyed(parentKeyColumns, elementClass)andMappedTableRecordKeyed(parentKeyColumns, elementClass); carry the developer-declaredClass<? extends TableRecord<?>>on the variant;keyElementType()returns it directly.ServiceCatalog.classifySourcesType’s `TableRecordelement branch reroutes onto the new permits, threading the typed class. Three downstream sites widen theirisMappedinstanceof checks to includeMappedTableRecordKeyed(RowsMethodShape.outerRowsReturnType,TypeFetcherGenerator.buildServiceDataFetcher,TypeFetcherGenerator.buildServiceRowsMethod).GeneratorUtils.buildKeyExtraction’s sealed switch grows one arm emitting `((Record) env.getSource()).into(Tables.X)withTables.Xresolved from the parent table; the rows-method emitter’sreturn ServiceClass.method(keys)line covers the new variants by construction (the lambda’skeyslocal is already typedSet<X>/List<X>, the developer’s signature matches, the call type-checks). The deferred-conversion comment onbuildServiceRowsMethoddrops out. Resolver-side parent-table consistency check:ServiceDirectiveResolver.validateTableRecordSourceParentTablerejectsSet<X>against a parent whose record class isn’tXwith a candidate-hint pointer; without it the typedinto(Tables.X)projection on a wrong-typed parent would silently produce nonsense. New helperBuildContext.recordClassForTypeName(parentTypeName)reads the@tabledirective on the parent type and looks up the catalog.MappedRowKeyed/RowKeyeddocstrings tighten to "onlySet<RowN<...>>/List<RowN<...>>classify here"; the variants are now shape-pure, matchingRecordKeyed/MappedRecordKeyedsiblings. Tests:BatchKeyTestextends the per-variant shape map with the two new permits (keyElementType()returnsFilmRecord,javaTypeName()yieldsjava.util.{List,Set}<...FilmRecord>);ServiceCatalogTest’s two existing `TableRecordcells flip fromRowKeyed/MappedRowKeyedtoTableRecordKeyed/MappedTableRecordKeyed. L5 + L6: newFilm.titleTitlecaseschema field paired withFilmService.titleTitlecase(Set<FilmRecord>) -> Map<FilmRecord, String>exercises the typed-record path end-to-end against the sakila PostgreSQL fixture;GraphQLQueryTest.films_titleTitlecase_resolvesViaServiceRecordFieldDataLoader_tableRecordSourceruns{ 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 toRecordKeyed/MappedRecordKeyedarms but typed to the developer’s element class). Open follow-ups (deferred): single-cardinality typed-record positional signature (X method(X parent)driven byLoaderDispatch.LOAD_ONE) ; confirm if the sameTableRecordKeyedpermit covers it cleanly when a real schema needs it; custom-scalarV-types in the typed-record map inheritRowsMethodShape.strictPerKeyType’s null-skip arm until R45 lands. Post-landing addition (c6a10133): composite-PK regression-guard cell `ServiceCatalogTest.reflectServiceMethod_compositeKeyTableRecordSources_classifiedAsMappedTableRecordKeyedpinningSet<FilmActorRecord>(2-PK) ontoMappedTableRecordKeyedso a future classifier collapse ontoMappedRowKeyedis caught at L1 rather than at consumer-build time; mirrors the consumer’sSet<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 ;parentTableextraction now resolves throughTableRef.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 withTableRecordkeys is structurally sound (the mapped DataLoader factory routes through the same hashing path that already works forRecordNkeys, andorg.jooq.impl.AbstractRecord.equals/hashCodeis 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-dbSUCCESS on Java 25; 1268 unit + pipeline tests pass; 186GraphQLQueryTestexecution 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’sDataFetchingFieldSelectionSet/SelectedFieldAPIs only exist inside a live query execution and cannot substitute. Theselection/package stays; wiring it into the@experimental_constructTypeclassifier is tracked separately. -
Promote
graphitron-testtographitron-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,64no longer points at the legacygraphitron-exampleon the retiredgraphitron-servletruntime) and the absence of a documented "how do I test my schema" answer for rewrite consumers. Stage 0 (4c2dc5d) splitsgraphitron-fixturesintographitron-sakila-db(catalog + jOOQ codegen) andgraphitron-sakila-service(Java service fixtures); renamesgraphitron-testtographitron-sakila-example; relocates the tier-annotation package (@UnitTier/@PipelineTier/@CompilationTier/@ExecutionTierunderno.sikt.graphitron.rewrite.test.tier) fromgraphitron-fixtures’s main source root into `graphitron’s test source root, republished as a `teststest-jar viamaven-jar-plugin’s `test-jargoal so import paths stay stable across the rename; updates every dependent (graphitron,graphitron-lsp, the twographitron-mavenITs,CLAUDE.md,.claude/web-environment.md,graphitron-rewrite/docs/{README,testing,rewrite-design-principles}.adoc, six javadoc/code comments,SampleQueryServiceandMutationPayloadLifterTestjavadoc). Stage 1 (b869b6e) layers Quarkus 3.34.5 + JAX-RS ontographitron-sakila-example: importsquarkus-bom, drops the test-scopehibernate-validator+expresslypair for compile-scopequarkus-hibernate-validator, addsquarkus-rest,quarkus-rest-jackson,quarkus-config-yaml,quarkus-jdbc-postgresql,quarkus-agroal, thequarkus-junit5+rest-assuredtest pair, and thequarkus-maven-pluginexecution. Hand-written runtime underapp/:GraphqlEngine(@ApplicationScoped, builds the schema once viaGraphitron.buildSchema(b -> {})),GraphqlResource(@Path("/graphql"), POSTapplication/json→application/graphql-response+jsonper the GraphQL-over-HTTP spec, GET for query-only, freshDataLoaderRegistryandAppContextper request stashed underGraphitronContext.classonExecutionInput),AppContext(implements GraphitronContext, per-requestDSLContextfrom the Quarkus-managedAgroalDataSourceplus a context-values map fed intogetContextArgument).application.yamlconfigures HTTP port and JDBC datasource via${VAR:default}env-var defaults pointing at thelocal-dbPostgres. One smoke test (GraphqlResourceSmokeTest+SmokeTestPostgresResourceQuarkusTestResourceLifecycleManager) POSTs{ customers { firstName } }and asserts 200 + non-empty; in-process query-to-database tests run unchanged alongside it.default-compilepinned 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 intosrc/test/java/.../querydb/(the four query-to-database testsGraphQLQueryTest,FederationEntitiesDispatchTest,FederationBuildSmokeTest,NoFederationRegressionTest) andinternal/(the seven generator-internal tests).IdempotentWriterTestrelocates fromgraphitron/src/test/java/no/sikt/graphitron/rewrite/intographitron-sakila-example/.../internal/with explicit imports forRewriteContextandGraphQLRewriteGeneratorsince the package-relative resolution no longer works after the move. Two new worked examples land underquerydb/:MatchQueryExampleTest(load.graphql, execute, assert specific paths) +customers_basic.graphql, andApprovalQueryExampleTest+films_basic.{graphql,approved.json}(execute, serialise to canonical JSON, compare; on divergence write a sibling.actual.jsonso the next iteration is "diff the two; mv onto approved if intentional").README.mdlands 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 forinternal/("you do not need to copy anything frominternal/`"). Stage 3 (4af7001) repoints `docs/quick-start.adoc:21,64atgraphitron-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-dbbuilds clean on Java 25 with the example module’s main jar compiled under Java 17; all 1643 tests pass (244 ingraphitron-sakila-example);mvn quarkus:devfrom 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 andtest-your-schema.adochow-to on. Out of scope and explicitly deferred: deleting legacygraphitron-example/(R26 owns that gating); HTTP-shaped query-to-database tests (the in-process pattern viagraphql-javastays canonical; the smoke test is the only HTTP-shaped check in the module); pedagogical schema simplification (getting-started.adocremains the on-ramp); test-pattern variants beyond approval + match (richer taxonomies are future follow-ups). -
Make the typed
Rejectionhierarchy 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 onUnclassifiedField/UnclassifiedType/ValidationErrorwith the sealedRejectionhierarchy and threads the typed shape from every producer site through every consumer. Top-levelAuthorError | InvalidSchema | Deferred, sub-sealedAuthorError.{UnknownName | Structural}andInvalidSchema.{DirectiveConflict | Structural},StubKey.{VariantClass(@Nullable Class<? extends GraphitronField>) | EmitBlock(EmitBlockReason)}, with a self-containedcandidateHintrenderer so the model package can render rejection messages without pulling inBuildContext.RejectionKindsurvives as a derived projection (RejectionKind.of(Rejection)) for the[<kind>] <message>log surface. Phase 0 (7c10226) dropsRejectionKind.INTERNAL_INVARIANT; the single producer atFieldBuilder.classifyChildFieldOnTableType’s nested-fields fallthrough becomes an `AssertionError. Phase A (09541ed) introduces the seal and liftsUnclassifiedFieldto carryRejection rejection; every classifierResolved.Rejectedarm widens. Phase B (5d29a3d) mirrors the lift ontoUnclassifiedType; 24 sites (21 inTypeBuilder, 3 inEntityResolutionBuilder); three table-resolution sites constructAuthorError.UnknownNameviaRejection.unknownTable. Phase C (68a062c) renamesTypeFetcherGenerator.NOT_IMPLEMENTED_REASONStoSTUBBED_VARIANTS(Map<Class, Rejection.Deferred>); the fourSplitRowsMethodEmitter.unsupportedReasonoverloads collapse ontoOptional<Rejection.Deferred>keyed byEmitBlockReason. Phase D (83816e0) walks the direct candidate-hint producers onto typedAuthorError.UnknownNamefactories (BatchKeyLifterDirectiveResolver,ServiceCatalogvia wideningServiceReflectionResult.failureReason: String → rejection: Rejection,FieldBuilderfor@nodeId(typeName:)/ column-on-FK-resolved-table / scalar-column-miss /DmlKindResult.Unknown); adds factoriesunknownTypeName,unknownEnumConstant,unknownNodeIdKeyColumn,unknownDmlKindand the leaf-armprefixedWith(String)instance method (used by the four wrapper sites that thread caller-specific prose ontoServiceReflectionResult.rejection); drops unusedAttemptKind.{TABLE_METHOD, ARGUMENT_NAME, FIELD_NAME}. Phases E–I (3dcd3c6): E replaces the nested-rewrap switch inFieldBuilder.classifyChildFieldOnTableTypewith a singleunc.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 liftsdetectChildFieldConflict,detectQueryFieldConflict, anddetectTypeDirectiveConflictfromStringtoRejection.InvalidSchema.DirectiveConflict; migrates explicit conflict sites (@service`@mutation+, `@notGenerated,@asConnection`@splitQuery+, `@asConnection`@lookupKey+` atLookupKeyDirectiveResolver);InvalidSchema.Structuralretains 5 classifier-side producers (root invariants, Connection-at-root for@tableMethod, single-cardinality@lookupKey, circular type,@errorfield shape) so the seal stays valid. G introduces theConditionJoinReportablecapability (unsealed, mirrorsBatchKeyField); the fourChildFieldvariants that share the condition-join predicate (SplitTableField,SplitLookupTableField,RecordTableField,RecordLookupTableField) implement it with their per-variantEmitBlockReasonanddisplayLabel; the fourunsupportedReasonoverloads collapse to one capability dispatch and the validator’s 4-arminstanceofchain collapses to a singleinstanceof ConditionJoinReportablecheck. H collapsesStubKey.Noneonto a nullableVariantClass.fieldClass(post-Phase-D the inline-Deferredproducer set is exactly 3 sites without natural variant-class anchors); the fourRejection.deferred(...)factories collapse to two:deferred(summary, planSlug, fieldClass)anddeferred(summary, planSlug). I liftsValidationErrorfrom(RejectionKind kind, String coordinate, String message, SourceLocation location)to(String coordinate, Rejection rejection, SourceLocation location)withkind()andmessage()projecting from the rejection; all 33 sites inGraphitronSchemaValidator, the 2 sites inGraphitronSchemaBuilder.buildRecipeErrors, and the watch-mode test fixture migrated;validateUnclassifiedField/validateUnclassifiedType/emitDeferredErroruseprefixedWithto 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, theunknownServiceMethodfour-wrapper prefix path, the directive-conflict cases (@service`@mutation+` and@table`@record+), the `ConditionJoinReportablecapability seal, and the validator-sideUnknownNamesurvival throughprefixedWithontoValidationError;RejectionRenderingTestextended with 8 model-tier cases for the new factories andprefixedWithpreservation 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.Mismatchjoined-prose aggregation,TypeBuilder.keyColumnErrors/failuresaggregation) tracked under R66 (rejection-string-carrier-widening); LSP fix-its consumingAuthorError.UnknownName.candidatesare R18; threading nested rejection chains as a typedRejection.NestedRejectarm deferred until error-aggregation consumers (LSP, watch-mode) demand it;ArgumentRef.UnclassifiedArg.reasonandBuildWarning.messagelifts (separate axes, single producers);RejectionKindrename. Build green:mvn -f graphitron-rewrite/pom.xml install -Plocal-dbSUCCESS 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) DropAccessorRowKeyedMany.Container: enum + record component gone;GeneratorUtils.buildAccessorRowKeyMany’s for-loop iterates any `Iterableso the LIST/SET split was never load-bearing. TheSet<X>vsList<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.Manyno longer carries the container;BatchKeyTestand theACCESSOR_ROWKEYED_MANY_*_ACCESSORpipeline cases dropped the enum-pinning assertions. (2)BatchKeyField#emitsSingleRecordPerKey()capability: new default method onBatchKeyFieldreturningfalse; overridden onSplitTableField(!returnType().wrapper().isList()) andRecordTableField(batchKey() instanceof AccessorRowKeyedMany). The two consumer sites ;TypeFetcherGenerator’s `scatterSingleByIdxhelper-emission gate andSplitRowsMethodEmitter.buildForRecordTable’s `buildSingleMethodrouting ; 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;RowKeyeddelegates toparentKeyColumns(), the three target-side arms (LifterRowKeyed,AccessorRowKeyedSingle,AccessorRowKeyedMany) delegate totargetKeyColumns()viahop.targetColumns().SplitRowsMethodEmitter.emitParentInputAndFkChain’s prelude param tightened from `BatchKeytoRecordParentBatchKey; thepkColsswitch withdefault -> throwcollapsed tobatchKey.preludeKeyColumns(). Helper-method chain (buildListMethod/buildSingleMethod/buildConnectionMethod) tightened to match. To carry the chain end-to-end,SplitTableField.batchKey()andSplitLookupTableField.batchKey()tightened fromBatchKey.ParentKeyedtoBatchKey.RowKeyed(which already implements bothParentKeyedandRecordParentBatchKey);deriveSplitQueryBatchKeyreturn type matches. The two@DependsOnClassifierCheckannotations on the prelude collapsed into one (the JOIN-on side claim aboutLiftedHop); the BatchKey-side claim is now load-bearing in the type system.TypeClassGenerator.collectBatchKeyColumns’s redundant `instanceof BatchKey.RowKeyedchecks became direct accessor reads onstf.batchKey().parentKeyColumns(). (4) Container/element classifier walk lifted intoServiceCatalog: newServiceCatalog.ContainerKind { SINGLE, LIST, SET }enum +ContainerSplitrecord +peelContainer(Type, Set<ContainerKind>)helper.classifySourcesType(SOURCES path) acceptsLIST | SET;FieldBuilder.classifyAccessorReturn(accessor path) accepts all three. Element-class checking (jOOQTableRecordsubtype, orRowN/RecordNparameterised raw on the SOURCES path) stays per-caller. Both call sites remain inside parse-boundary classes; the shape walk has one home. (5) TypedLoaderDispatchprojection: newBatchKey.LoaderDispatch { LOAD_ONE, LOAD_MANY }enum andRecordParentBatchKey#dispatch()accessor; the three single-key arms returnLOAD_ONE,AccessorRowKeyedManyreturnsLOAD_MANY.TypeFetcherGenerator.buildRecordBasedDataFetcherreadsbatchKey.dispatch()once and forks the loader value type and dispatch call shape on the projection (replacing the inlineinstanceof AccessorRowKeyedMany). The@DependsOnClassifierCheckannotation onbuildRecordBasedDataFetcherwas rewritten to reference thedispatch == LOAD_MANYrule; the producer-side description onaccessor-rowkey-cardinality-matches-fieldwas tightened in the same pass to drop the obsoleteusesLoadManyterm. (6) Delete unusedListAccessorOnSingleFieldfixture: the record had javadoc noting it existed "for symmetry"; no test referenced it. The unusedFilmActorRecordimport dropped with it. Deviations from spec: (a) Item 5 implemented as enum rather than the spec’s proposed sealedLoaderDispatch { 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’sSplitTableField/SplitLookupTableFieldbatchKey()tightening was not explicitly called out by the spec, but proved necessary to type the prelude parameter asRecordParentBatchKeyend-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:emitsSingleRecordPerKeyis aBatchKeyField-level question (rows-method shape; depends on field cardinality);dispatchis aRecordParentBatchKey-level question (loader call shape). They coincide forRecordTableFieldwithAccessorRowKeyedMany(bothtrue/LOAD_MANY) but diverge for single-cardinalitySplitTableField(emitsSingleRecordPerKey == true, nodispatchprojection ;SplitTableFieldcarriesRowKeyedwhosedispatch()isLOAD_ONEregardless of field cardinality). Verification:mvn -f graphitron-rewrite/pom.xml install -Plocal-dbSUCCESS on Java 25; 1262 unit + pipeline tests pass; 238 graphitron-test compilation + execution tier tests pass;LoadBearingGuaranteeAuditTestno orphans. Out of scope (unchanged): renamingAccessorRowKeyedSingle/AccessorRowKeyedMany(names accurately reflect cardinality at the variant level); theSinglepermit’s emitter wiring (already complete in R60; execution-tier coverage gap tracked under the validator’s Invariant #10 lift);RecordBatchKeyResolutionand theAccessorDerivation/AccessorMatchtwo-stage builder hierarchy (clean applications of "Builder-step results are sealed"). -
@servicerows-method body ; strict return-type validation + shape lift to model (service-rows-method-body, R32,64b8e2c+e28540b+83bcfdf): closes out R32 by mirroringServiceCatalog.reflectServiceMethod’s strict-return check on the child `@servicepath 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 asbefc156) shipped earlier; this entry covers iterations 2 and 3 plus a review-pass nit. Iteration 2 ; strict child-@servicevalidation:ServiceDirectiveResolver.validateChildServiceReturnTyperejects developer methods whose declared return type doesn’t structurally match the rows-method’s outer shape. Per-keyVderives fromReturnTypeRef; raworg.jooq.RecordforTableBoundReturnType, the backing class forResultReturnTypewith non-nullfqClassName, the standard Java type for the five standard GraphQL scalars (String/Boolean/Int/Float/ID); other cases (custom scalars, enums,PolymorphicReturnType,ResultReturnTypewith no backing class) skip the strict check. Carries theservice-directive-resolver-strict-child-service-return@LoadBearingClassifierCheckkey, paired with@DependsOnClassifierCheckonTypeFetcherGenerator.buildServiceRowsMethod; the emitter can now emitreturn ServiceClass.method(<args>);against a structurally-typed return without a defensive cast or wildcard local. Author errors surface at classify time rather than asjavacerrors on the generated source. Iteration 3 ; lift rows-method shape onto the model: validator and emitter each used to reconstructMap<K, V>/List<List<V>>/List<V>from(returnType, batchKey)independently, the per-keyVderivation lived a third time onChildField.ServiceRecordField.elementType()with a deliberately-divergent fallback, andGeneratorUtils.keyElementTypehad been bumped topublicso the classifier-tier validator could import from the generators package. The shared form lives in two new model-package surfaces:BatchKey.keyElementType()(adefaultaccessor on the sealed root, replacing the static helper inGeneratorUtils) andRowsMethodShape.{strictPerKeyType, outerRowsReturnType, standardScalarJavaType}(the per-keyVdecision and the(isMapped, isList)outer-shape construction). Validator and emitter both callRowsMethodShape.outerRowsReturnType(perKey, returnType, batchKey); only theperKeyinput differs (validator:RowsMethodShape.strictPerKeyTypeand skip on null; emitter: the field-knownVfrom the literalRECORDconstant orsrf.elementType()). The@LoadBearingClassifierCheck/@DependsOnClassifierCheckpair still holds the contract at audit time, but the construction can no longer drift across sites.GeneratorUtils.keyElementTypeis 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 invalidateChildServiceReturnTypeso theParam.Sourcedfilter usesclass::isInstance/class::castand assigns to a typed local before reading.batchKey(). Behaviour-preserving; null-tolerance contract unchanged. Tests:GraphitronSchemaBuilderTest.UnclassifiedFieldCase.CHILD_SERVICE_TABLE_BOUND_WRONG_RETURN_REJECTED(declaredLanguageRecordinstead ofList<Record>) andCHILD_SERVICE_SCALAR_WRONG_VALUE_TYPE_REJECTED(declaredMap<Record1<Integer>, Integer>for aString-valued field) pin the two rejection arms; the previously-shippedGraphQLQueryTest.films_titleUppercase_resolvesViaServiceRecordFieldDataLoadercontinues to exercise the end-to-end positive path against PostgreSQL. The dropped positive cell collapses onto the validator-and-emitter sharedRowsMethodShape.outerRowsReturnTypecall so structural drift between them is no longer reachable. Open follow-ups (deferred or tracked elsewhere): element-shape conversion when the developer’sSourcesisSet<TableRecord>/List<TableRecord>(deferred until a real schema needs it; builds on top of R61); theRow1→Record1framework switch (R61,emit-record1-keys-instead-of-row1.md);ParamSource.Context’s typed registry (tracked under `typed-context-value-registry.md). -
Auto-derive
BatchKeyfrom typedTableRecordaccessor on@recordparents (auto-derive-batchkey-from-typed-record-accessor, R60,14889c1+aabd7ea+b2ae55d): closes the@record-parent free-form-DTO rejection inFieldBuilder.classifyChildFieldOnResultTypefor the case where the parent class already exposes a typed zero-arg instance accessor returning a concrete jOOQTableRecord(single,List<X>, orSet<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>forX extends TableRecordwhose mapped table equals the field’s@tablereturn), and produces one of two newBatchKey.RecordParentBatchKeypermits ;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 jOOQTableRecord) replaces the previous two-option message. Model: newAccessorRef(parentBackingClass, methodName, elementClass)carries pre-resolved javapoetClassName`s, sibling of `LifterRef;BatchKey.RecordParentBatchKey’s permit list grows from 2 to 4 (still permits `RowKeyed+LifterRowKeyed); both new permits delegatetargetKeyColumns()toJoinStep.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-methodAccessorMatch.{Single, Many, CardinalityMismatch}and call-resultAccessorDerivation.{Ok, None, Ambiguous, CardinalityMismatch}capture the reflection match and reduction respectively, neither leaking pastFieldBuilder(Principle 7 "Builder-internal sealed hierarchies for multi-target classification"). The accessor-arm rewrites thejoinPathto[liftedHop]soSplitRowsMethodEmitter’s prelude reads target accessors uniformly through `JoinStep.WithTarget. Emitter:GeneratorUtils.buildRecordParentKeyExtraction’s switch grows from 2 to 4 arms; `buildAccessorRowKeySingleemits((BackingClass) env.getSource()).<accessor>()followed byDSL.row(__elt.get<Pk>(), ...);buildAccessorRowKeyManyemits a typed for-loop over the accessor’sIterablereturn building aList<RowN<...>>forloader.loadMany.TypeFetcherGenerator.buildRecordBasedDataFetcherswitches the loader value type toRecord(1:1 with element-PK keys) and the dispatch toloader.loadMany(keys, Collections.nCopies(keys.size(), env))when the BatchKey isAccessorRowKeyedMany; result type still follows the field’s GraphQL cardinality.SplitRowsMethodEmitter.buildForRecordTableroutesAccessorRowKeyedManythroughbuildSingleMethod(1 record per key,scatterSingleByIdx) rather thanbuildListMethod;buildSingleMethodwidens its first-hop cast fromJoinStep.FkJointoJoinStep.WithTargetwith a conditionalwhereFilterlift, since bothFkJoin(single-cardinality SplitTableField) andLiftedHop(loadMany-many) reach it.TypeFetcherGenerator.hasSingleSplitFieldwidens to also gatescatterSingleByIdxemission on anyRecordTableFieldcarryingAccessorRowKeyedMany. The shared prelude’spkColsswitch inSplitRowsMethodEmitter.emitParentInputAndFkChainadmits all fourRecordParentBatchKeypermits viatargetKeyColumns()(lifter / accessor) andparentKeyColumns()(RowKeyed). Load-bearing keys: two new@LoadBearingClassifierCheckkeys ;accessor-rowkey-shape-resolved(the producer guarantees the parent backing class, the accessor identity, and the element class are all reflectively confirmed before emittingAccessorRowKeyedSingle/AccessorRowKeyedMany; consumed bybuildAccessorRowKeySingle/buildAccessorRowKeyManywhich castenv.getSource()and invoke the accessor without instanceof or null guards) andaccessor-rowkey-cardinality-matches-field(the producer pairsAccessorRowKeyedManywith list-cardinality fields andAccessorRowKeyedSinglewith single-cardinality; consumed bybuildRecordBasedDataFetcher’s `usesLoadMany ⇔ valueType = Recordrule).LoadBearingGuaranteeAuditTestpasses; no orphans. Test surface: unit-tierBatchKeyTest(5 cases, including a four-permit exhaustive-switch compile pin); pipeline-tierAccessorDerivedBatchKeyCase(6 cases ; list × list-accessor / list × set-accessor / single × single-accessor / ambiguous candidates / cardinality-mismatch / heterogeneous element); execution-tierAccessorDerivedBatchKeyTestruns theManyend-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)Containerenum onAccessorRowKeyedManyis preserved on the model but the for-loop iterates anyIterable, so emit no longer forks on it (acknowledged technical debt; tracked under R65 #1 follow-up); (b)Manyrows-method routes throughbuildSingleMethodrather than a new shape, sinceloadManyis 1:1 record-per-key by contract; (c)loadManyoverload requiresList<Object>of key contexts, so the dispatch passesCollections.nCopies(keys.size(), env)and the batch loader readskeyContexts[0]as before; (d)Singlepermit is fully wired through the emitter but blocked at validate-time by Invariant #10 (single-cardinalityRecordTableFieldrejection), so end-to-end execution-tier coverage waits for that gate to lift. Reviewer-pass follow-up:R65 accessor-batchkey-emitter-tighteningfiled for six architectural cleanups surfaced during the review (Containerslot,hasSingleSplitFieldpredicate union, preludedefault ->arm, two-classifier reflection-walk dedup, two-site dispatch fork inbuildRecordBasedDataFetcher, unusedListAccessorOnSingleFieldfixture). Build green:mvn -f graphitron-rewrite/pom.xml install -Plocal-dbSUCCESS on Java 25; 1247 unit/pipeline tests + execution tier all pass. -
Lift
@lookupKeypartition ontoTableInputArg(dml-lookup-key-partition-on-tableinputarg, R62,b4624f4): addslookupKeyFieldsandsetFieldsprojections toArgumentRef.InputTypeArg.TableInputArg, populated once via a newTableInputArg.of(...)factory at the two construction sites (FieldBuilder.classifyArgument,MutationInputResolver.resolveInput). The narrowList<InputField.ColumnField>element type expresses the mutation-arm guarantee that DML inputs admit onlyDirect-extractedColumnField; query-side TIAs simply contribute zero entries because@lookupKeylands only on aColumnField. Three consumers drop their ad-hocSet<String>rebuild:MutationInputResolverInvariant #4 readssetFields().isEmpty();buildMutationUpdateFetcherwalkstia.setFields()(no skip-during-walk, no cast);buildMutationUpsertFetcherwalkstia.setFields()for the SET clause and reads!setFields().isEmpty()for the.doUpdate()/.doNothing()dispatch (the col/val lists still walkfields()to keep@lookupKeyfields on the insert branch).dml-mutation-shape-guaranteesconsumerreliesOnstrings updated to drop the "skip-the-set-during-walk" phrasing;LoadBearingGuaranteeAuditTestcontinues to pair the producer (FieldBuilder.buildDmlField) with the four emitter consumers. Pipeline coverage inGraphitronSchemaBuilderTest(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 in181c28f. -
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 ofTypeFetcherGenerator.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): sharedDmlTableFieldsealed supertype permits the four DML records, all sharing(parentTypeName, name, location, returnType, tableInputArg, encodeReturn, errorChannel); oneFieldBuilder.classifyMutationInput(fieldDef, typeName)helper enforces Invariants #1 through #14 across all four DML verbs; the mutation-arm switch inclassifyMutationFieldbuilds the appropriate variant record from the resolved tia + encodeReturn. TheMutationField.DmlTableFieldlift and the four DML records actually shipped as part of R50’s cleanup pass (R50 deletedInputField.NodeIdField,NodeIdReferenceField,IdReferenceField,NodeIdInFilterFieldand folded their cross-table cases underColumnReferenceField/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 onDmlTableFieldwith a single sealedDmlReturnExpression returnExpressionslot. Five arms (EncodedSingle,EncodedList,ProjectedSingle,ProjectedList,Payload) cover exactly Invariant #14’s admitted return-type set; thePayloadarm absorbs the R12-introducedOptional<PayloadAssembly>. Records went from 8 components to 6; emitters pattern-match a single sealed dispatch with noinstanceof ScalarReturnType/wrapper().isList()/Optional.orElseThrow()/payloadAssembly().isPresent()predicates. New load-bearing keydml-mutation-shape-guaranteesannotatesFieldBuilder.buildDmlField(producer) and the four DML emitters (consumers);LoadBearingGuaranteeAuditTestenforces the pairing. Phase 2 (INSERT,d792463):buildMutationInsertFetcherplus the verb-neutralbuildDmlFetcherskeleton (try/catch envelope,dslchain,payloadbind,returnSyncSuccess/catchArm) and theemitDmlReturnExpressionprojection terminator extracted from DELETE; column list and parallel values list both walktia.fields()once, values useDSL.val(in.get(name), Tables.T.COL.getDataType())for converter-mediated coercion. Execution-tiercreateFilm_insertsRowAndReturnsProjectedFilmagainst PostgreSQL verifiesRETURNING $fieldsend-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 samebuildDmlFetcherskeleton (buildMutationDeleteFetcherpattern-matches onf.returnExpression()viaemitDeleteEncoded/emitDeleteProjected/emitDeletePayloadhelpers, noinstanceofpredicates). Phase 4 (UPDATE,4dc4c04):buildMutationUpdateFetchershares the same skeleton; SET clause walkstia.fields()skipping@lookupKeynames, WHERE clause reusesbuildLookupWhere. Execution-tierupdateFilm_updatesRowAndReturnsProjectedFilmagainst PostgreSQL inserts a marker row, runs the mutation, asserts the SET clause wrote andRETURNING $fieldsreturned the new title withlanguageIdcarrying through unchanged. Phase 5 (UPSERT,2e9712e):buildMutationUpsertFetcheragainst the same skeleton; INSERT col/values lists walktia.fields()once (every field,@lookupKeyincluded), SET clause skips@lookupKeynames,.onConflict(<keys>)reads fromtia.fieldBindings(). Empty-SET case emits.doNothing()(jOOQ rejects.doUpdate()with no.setcalls). UPSERT additionally carries an Oracle-dialect runtime guard (jOOQ silently translatesINSERT ... ON CONFLICTtoMERGE INTOwith 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. BothMutationServiceTableFieldandMutationServiceRecordFieldun-stubbed by delegating to the sharedbuildServiceFetcherCommonhelper; the R12 §3 try/catch wrapper, §5 Jakarta validation pre-step, and §2cresultAssemblysuccess-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.unsupportedReasonreturningOptional<Rejection.Deferred>only to have callers immediately call.message()to feedbuildRuntimeStub’s `Stringparameter, dropping the typedEmitBlockReason) lives in R58’s domain rather than R22’s and is captured asruntime-stub-takes-deferred-rejection(R64). Out of scope and tracked separately: listed inputs (in: [FilmInput]), nested@tableinputs (NestingField),@nodeId-typed input fields (NodeIdDecodeKeys-extractedColumnField),ColumnReferenceField/CompositeColumnField/CompositeColumnReferenceFieldin 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-TableBoundReturnTypereturn types on DML fields (Int/Boolean/Connection<T>rejected at classify time; anAffectedCountarm onDmlReturnExpressionis the future lift if needed),ScalarReturnType(ID)on non-@nodetables (rejected with descriptive message), transaction wrapping (caller’s responsibility viadsl), non-PostgreSQL dialects (RETURNINGandON CONFLICTare Postgres-specific; UPSERT additionally carries the Oracle runtime guard),@mutation+@servicemutual-exclusion (already rejected at classifier time). Build green:mvn -f graphitron-rewrite/pom.xml install -Plocal-dbSUCCESS 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.ServiceCatalogparameter-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 viaargMapping: "<javaParam>: <graphqlArg>"on@service); the empty-args branch suggests removing the parameter, adding a GraphQL argument, or registering a context key.MutationInputResolverlisted-@table-input rejection (MutationInputResolver.java:249-255): names the supported single-non-list@tableinput wrapper shape and points at the bulk-mutation roadmap gap.FieldBuilderpayload-multi-ctor rejection (FieldBuilder.java:1486-1492and:1600-1606): lists found ctor signatures via the sharedformatCtorSignatureshelper and suggests record conversion or removing extras.BuildContextzero-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:RecordTableFieldandRecordLookupTableFieldfree-form-DTO rejections inFieldBuilder.classifyChildFieldOnResultTypeflip fromRejectionKind.DEFERREDtoAUTHOR_ERRORsince R1’s@batchKeyLifteralready 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-stylefile:line:col: error:lines relativise againstctx.basedir()viaGraphQLRewriteGenerator.relativiseSourceNameso 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*Fieldvariants out ofNOT_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):QueryTableInterfaceFieldandChildField.TableInterfaceFieldemit a single SELECT against the discriminator-bearing parent table with per-participant LEFT JOINs gated ondiscriminatorColumnand per-occurrence aliases for cross-table@referenceparticipant fields (lifted asChildField.ParticipantColumnReferenceField);buildDiscriminatorFilterfires the discriminator predicates as a SQLIN (...)clause;JooqCatalog.findColumnresolves SQL column names from logical names; selection-set gating uses graphql-java 25’sType.fieldform. Track B (multi-table polymorphic, two-stage shape): newMultiTablePolymorphicEmitterproduces a stage-1 narrow UNION ALL projecting(typename, pk0..pkN, sort)across participants, then dispatches stage-2 per typename throughValuesJoinRowBuilder(the same row-builder R55 collapsed for_entities/Query.nodes/@lookupKey) with explicit per-PK-slott.<col>.eq(input.field(...))ON predicates. Composite PKs projectDSL.jsonbArray(...)as__sort__so cursor-decode round-trips viaConnectionHelperClassGenerator.encode/decode’s existing JSONB conversion; child fields auto-discover their FK join paths via `FieldBuilder.ctx.parsePathper(parentTable, participantTable)pair. Connection mode: root and child connections sharebuildStage1ConnectionBlock, which lifts the per-branch UNION ALL into aTable<?> pagesTableso the same derived-table reference backs the page query and theConnectionResulttotalCount. Child-connection emission uses a DataLoader-batched windowed CTE (buildBatchedConnectionFetcherplusbuildBatchedConnectionRowsMethod): typedparentInput VALUESwidens toRow<N+1>for composite-PK parents (capped at 22 to fit jOOQ’s typed Row22 ceiling), per-branchJOIN parentInput ON <participant>.<fk> = parentInput.<parent_pk>emits the position-aligned composite-FK AND-chain, and aROW_NUMBER() OVER (PARTITION BY __idx__ ORDER BY page.effectiveOrderBy())outer filter caps each parent’s rows atpage.limit(); per-parentConnectionResultshares onepagesTable. The page-rows query collapses N parents to 1 SQL statement, asserted byaddressOccupantsConnection_dataLoaderBatchesAcrossParentsandprojectItemsConnection_dataLoaderBatchesAcrossParents. Validator:validateMultiTableParticipantsrejects PK-less participants and PK-arity mismatches;validateChildConnectionParentPkrejects empty parent PK and parent-PK arity > 21 as build-timeAUTHOR_ERRORinstead of codegen-timeIllegalStateException. The earliervalidateMultiTableConnectionConstraints(the arity-1 reject from B4a) deletes once Item 1 generalises connection mode to composite-PK participants via the JSONB sort key. TypeResolver wiring:GraphitronSchemaClassGeneratoriterates non-NodeInterfaceType/UnionTypealphabetically and reads the synthetic__typenamecolumn projected by stage-1. Surface collapse (Item 3): the 5-arg and 6-argemitConnectionMethodsoverloads and the per-parent inline branch retire (B4c-1 was promoted same-day to B4c-2’s batched form); the dispatcher inemitConnectionMethodsis now a singleparentTable != nullswitch on the 7-arg signature, called directly fromTypeFetcherGenerator’s four interface / union arms. Test surface: unit-tier coverage in `TypeFetcherGeneratorTest,GraphitronSchemaClassGeneratorTest,InterfaceFieldValidationTest, andUnionFieldValidationTest(1219 total); execution-tier (233 total) covers cross-table participant fields, multi-table polymorphic root and child, connection pagination plus after-cursor plustotalCountplus inline-fragment dispatch, multi-parent DataLoader batching ratchet, composite-PK participants viapaged_a/paged_bfixture, and composite-PK parents via syntheticproject (org_id, project_id)withproject_note/project_eventchildren. 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@nodeIdprojections continue in stage-2’s typed Record path);NodeIdReferenceFieldJOIN-projection form (R50 follow-up);Nodeinterface TypeResolver (already wired viaQueryNodeFetcher.registerTypeResolver). Priority number#3is embedded in emitted reason strings consumed by existing schema authors and must stay stable. Build green:mvn -f graphitron-rewrite/pom.xml install -Plocal-dbSUCCESS on Java 25; 1219 unit plus 233 execution all pass. -
Argument-level
@nodeIdarchitectural tightenings (argument-level-nodeid, R40,5064a16+9192bf7+5891293+9232887): the argument-level@nodeIdmachinery was already shipping correct user-visible behaviour out of R50; this item closes three structural seams a design review surfaced. Phase 1:Resolved.FkTargetsplits intoDirectFk/TranslatedFksub-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 inlinesameColumnsBySqlNamecheck deletes fromFieldBuilder.BuildContext.classifyInputFieldwas previously not running the predicate at all, silently letting the pathological FK-target shape through; the lift closes that asymmetric-gating gap. The sharedtranslatedFkRejectionReasonmethod names the R57 hint substring. Phase 2:LookupValuesJoinEmitter.addRowBuildingCorebranches the per-row decode site onCallSiteExtraction.NodeIdDecodeKeys.ThrowOnMismatchkeeps the existingGraphqlErrorExceptionfor synthesised lookup-key paths where a wrong-type id is a contract violation;SkipMismatchedElementemitscontinueand tracks aneffectiverow count, returningArrays.copyOf(rows, effective)when shrunk.LookupMapping.LookupArg.DecodedRecordretypes its decode slot fromHelperRef.DecodetoCallSiteExtraction.NodeIdDecodeKeysso the failure-mode arm rides on the model.FieldBuilder.classifyArgument’s same-table arg arm flips from `ThrowtoSkip, restoring the originally-specified Skip semantics over the first pass’s expedientThrow; the implicit scalar-IDarm (no@nodeId, NodeId-backed table) keepsThrowfor the synthesised lookup-key path. Phase 3:NodeIdArgPlanpre-resolves every@nodeId-decorated leaf reachable from a table-bound field’s argument set in one walk, threaded throughresolveTableFieldComponents → classifyArguments → classifyArgument; the three previous walks (findSameTableNodeIdUnderAsConnection,walkInputTypeForSameTableNodeId,hasSameTableNodeIdAnywhere) collapse into reads of the plan. The@asConnectionrejection, 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-matchannotatesNodeIdLeafResolver.resolve(producer) and three consumers (FieldBuilder.projectFilters,FieldBuilder.walkInputFieldConditions,BuildContext.classifyInputField);LoadBearingGuaranteeAuditTestpicks up the pairing automatically. Test surface: pipeline-tierInputFieldFkTargetNodeIdCase.FK_TARGET_PATHOLOGICAL_KEY_MISMATCH_DEFERRED_INPUTcovers the input-field-side asymmetric-gating closure;ArgumentSameTableNodeIdCaseextraction assertions flipped fromThrowOnMismatchtoSkipMismatchedElement. Resolver-tierNodeIdLeafResolverTestis 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 targetsparent_node.alt_keybut the NodeType key isparent_node.pk_id, DirectFk again on the input-field side); a newGraphitronSchemaBuilder.buildContextForTestsseam exposes the wiredBuildContextafter type classification but before field classification. Execution-tierGraphQLQueryTesttriplet (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 (TranslatedFkJOIN-with-translation emission), multi-hop FK-target on the input side, mutation-key@nodeIdargs, andRecord1raw-cast template factoring. Fullmvn install -Plocal-dbclean. -
@batchKeyLifterdirective re-enables DataLoader batching on@recordparents that lack catalog FK metadata (batchkey-lifter-directive, R1,07e6954+d12e60d+4283fb2+7c284a1+b5c6749+a1a5e29): closes the twoRecordTableField/RecordLookupTableField"requires a FK join path and a typed backing class for batch key extraction" deferred rejections inFieldBuilder.classifyChildFieldOnResultTypefor free-form DTO parents (PojoResultTypeandJavaRecordTypewith non-nullfqClassName). The schema author supplies a static Java method that lifts aRowN<...>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’stargetColumns, and produces aBatchKey.LifterRowKeyedcarrying aJoinStep.LiftedHop(target table + key columns, single-hop by construction) plus aLifterRef(ClassName, String)typed reference (sibling ofMethodRef, shaped after R50’sHelperRefprecedent). The emitter feeds the result into the existing column-keyed DataLoader path with no identity branching: target accessors come from a newJoinStep.WithTargetcapability mixed in byFkJoinandLiftedHop; key extraction comes from the lifter call. Surface: new directive on FIELD_DEFINITION (@batchKeyLifter(lifter: ExternalCodeReference!, targetColumns: [String!]!)); newBatchKey.LifterRowKeyedpermit (sealed hierarchy now five variants:RowKeyed,RecordKeyed,MappedRowKeyed,MappedRecordKeyed,LifterRowKeyed); newBatchKey.ParentKeyedandBatchKey.RecordParentBatchKeysealed sub-interfaces splitting the variant axis (the four catalog records exposeparentKeyColumns()renamed fromkeyColumns();LifterRowKeyedexposestargetKeyColumns()via the containedLiftedHop); the interface-levelBatchKey.keyColumns()accessor removed (a shared accessor with variant-dependent meaning violated Sealed hierarchies over enums); newJoinStep.LiftedHoppermit (sealed hierarchy now three variants:FkJoin,ConditionJoin,LiftedHop); newJoinStep.WithTargetcapability mixed in byFkJoinandLiftedHop; newBatchKeyLifterDirectiveResolverstandalone resolver, sibling to R6’s ten directive/projection resolvers, so classifier-side directive logic stays out ofFieldBuilder. Renames and narrowings:GeneratorUtils.buildRecordKeyExtraction→buildRecordParentKeyExtraction, parameter narrowed fromBatchKeytoBatchKey.RecordParentBatchKey;GeneratorUtils.buildKeyExtractionparameter narrowed fromBatchKeytoBatchKey.ParentKeyed; both narrowings turn mis-routing of the@service-only permits into compile errors rather than runtime throws.TypeFetcherGenerator.buildRecordBasedDataFetcherno longer casts toBatchKey.RowKeyed.SplitRowsMethodEmitter.emitParentInputAndFkChainreads target accessors uniformly via theJoinStep.WithTargetcapability; 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):RecordTableFieldandRecordLookupTableFieldreject single-cardinality returns at validate time, promoting the previousSplitRowsMethodEmitter.unsupportedReasonruntime stub to a build-time AUTHOR_ERROR. The stub is replaced by anIllegalStateException(post-validate reachability is a classifier bug). Emitter fix (Phase 2f):SplitRowsMethodEmitter.buildListMethodWHERE-filter loop unconditionally cast everyJoinSteptoFkJoin, throwingClassCastExceptionforLiftedHoppaths. Fixed withif (!(path.get(i) instanceof JoinStep.FkJoin hop)) continue;;LiftedHopcarries no FK-side filter to apply, so the loop skips it. Load-bearing keys: per-fact@LoadBearingClassifierCheck/@DependsOnClassifierCheckpairslifter-classifies-as-record-table-fieldandlifter-batchkey-is-lifterrowkeyed(both with producer pairs onBatchKeyLifterDirectiveResolver.resolve). The single-hop invariant is a structural model property (LifterRowKeyedholds oneLiftedHop, not a list) documented in a plain javadoc comment on the rows-method prelude rather than as a keyed fact.LoadBearingGuaranteeAuditTestis unchanged. Test surface:BatchKeyLifterCasepipeline-tier coverage of the classifier matrix (POJO_PARENT_VALID_ROW1_LISTetc.) plus scalar-return rejection;MutationPayloadLifterTestexecution-tier coverage with threeCreateFilmPayloadrows (languageId[1, 2, 1]) asserting one DataLoader dispatch with two distinct keys (DataLoader key-deduplication: 3 input rows → 2 batched VALUES tuples), the SQL containslanguage_idand"language"and(values (0, ?), (1, ?)), and per-parentLanguagelists resolve correctly (English× 2,Italian× 1). Hand-rolled fixture service (noDSLContextparameter) ensures theQUERY_COUNT == 1assertion is clean ; the only JDBC round-trip is the lifter-batched language lookup. Documentation: rejection messages inFieldBuilderandServiceCatalog.dtoSourcesRejectionReasonnow reference the live directive instead of the roadmap-file path;code-generation-triggers.adocdirective table gains a@batchKeyLifterrow on the@record-parent child-fields table and the Source Map’sBatchKey/JoinSteppermit listings updated for the new variants;rewrite-design-principles.adocgains a "DTO-parent batching" subsection cross-linked from "Column value binding". Fullmvn install -Plocal-dbclean. -
Load-bearing classifier guarantee audit annotations (
load-bearing-guarantee-audit, R21,9acdf3f): codifies the "classifier rejection becomes emitter assumption" pattern named inrewrite-design-principles.adoc § "Classifier guarantees shape emitter assumptions"as a runtime-discoverable annotation pair underno.sikt.graphitron.rewrite.model.LoadBearingClassifierCheck(key, description)marks the producer arm;DependsOnClassifierCheck(key, reliesOn)(repeatable viaDependsOnClassifierChecks) marks each emitter site that relies on it.LoadBearingGuaranteeAuditTestwalkstarget/classesunder the rewrite package root, groups by key, and fails on (a) any consumer key without a matching producer, (b) any duplicate producer key, (c) blankdescription/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-privateaudit(Iterable<Class<?>>)returningList<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: producersservice-catalog-strict-tablemethod-return(ServiceCatalog.reflectTableMethod) andservice-catalog-strict-service-return(ServiceCatalog.reflectServiceMethod) paired withTypeFetcherGenerator.buildQueryTableMethodFetcher/buildQueryServiceTableFetcherconsumers;column-field-requires-table-backed-parent(FieldBuilder.classifyChildFieldOnTableType) paired withTypeFetcherGenerator.generateTypeSpec’s `case ChildField.ColumnFieldarm;error-channel.mappings-constant(FieldBuilder.resolveErrorChannelpaired withErrorMappingsClassGenerator.generate); plus the consumerlesserror-type.path-message-fieldsproducer onTypeBuilder.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*Fetcherscompile 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 of9acdf3f(R12 §2c) because R12’serror-channel.mappings-constantwas the first new live producer and the infrastructure was needed to gate it. -
EntityFetcherDispatchper-typeId VALUES emission collapsed onto a shared row-builder (entityfetcherdispatch-lookup-pipeline-collapse, R55,5aec7cd+8ac503c+aee21f6): the typedRow<N+1>array, the arity-22 cap, the per-cellDSL.val(value, table.COL.getDataType())construction, theDSL.values(rows).as(alias, "idx", "<sqlName>", …)aliasing, and the USING-args list now live in one place. NewValuesJoinRowBuilderhelper (graphitron/src/main/java/no/sikt/graphitron/rewrite/generators/util/ValuesJoinRowBuilder.java) is consumed by bothLookupValuesJoinEmitter(@lookupKeyroot and inline-child paths) andSelectMethodBody(federated_entitiesplusQuery.node/Query.nodesdispatch); the f-E SQL-shape pin (GraphQLQueryTest.nodes_perTypeIdBatch_emitsValuesJoinOrderByIdxShape) gates that thevalues/join/order bysubstring shape survives across both call sites. Helper API generic over caller slot: methods takeList<S>plusFunction<S, ColumnRef>projection plus a directive-contextString(used in arity-cap and empty-slots error messages). The lookup site keeps its richSlotrecord (argName, RootSource, decode bindings) and passesSlot::targetColumn; the dispatcher passesFunction.identity()againstList<ColumnRef>. ThecellsCodevalue-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 fillsrows[i](lookup site does composite-key extraction and the per-rowDecodedRecordNodeId decode +GraphqlErrorExceptionon null; dispatcher readsbinding[0]/binding[1]); the idx cell expression (lookup usesDSL.inline(i), dispatcher usesDSL.val(idx, Integer.class), both render to a typedField<Integer>); any extra projections beyond the join (the dispatcher’sDSL.inline("<TypeName>").as("__typename")and the materialisedidxCol); the join syntax; and the.where(condition)/.orderBy(idxCol)chain. Reviewer-pass deltas (commit8ac503c): 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 referencest.<col>directly, andUSINGcollapses joined columns at render time, risking interactions with$fields-emitted projections that include the joined key columns themselves. The helper still exposesusingArgsfor the lookup root path; the dispatcher’s join syntax is documented onSelectMethodBody’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 foldsnoCondition()away at render time. Other reviewer-pass deltas: dropped a transitionalValuesJoinRowBuilder.Slot(ColumnRef)record that required parallel-list bookkeeping at the lookup site; added an empty-slots guard torowTypeArgs(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_ARITYis package-private (tests are in-package, no external caller). Test surface: 16ValuesJoinRowBuilderTestcases 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 (lookupDSL.inline(i), dispatcherDSL.val(idx, Integer.class)), the rich-slot callback contract, and theRow<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:LookupValuesJoinEmitter505 → 458,SelectMethodBody164 → 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 (commitaee21f6): dispatcher’sColumnRefprojection switched fromc -> ctoFunction.identity()(singleton, saves one lambda allocation per emit); unusedSlotlambda 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. -
FieldBuilderdecomposed onto the cross-cutting-concern axis (decompose-fieldbuilder, R6, Phase 1 at3f9b84c; Phases 2a/2b/2c at201c2f0+c819027+9e8fc46; Phase 5 atfd94f37; Phases 6a/6b/6c/6d/6e at38b143c+67c543d+9766982+679c560+56cd3a0; Phase 7 at42f8259; Phase 8 final mop-up at84f4be7; review-driven shape tightening atcea16e0): the parent-context-first dispatch (classifyQueryField/classifyMutationField/classifyChildField*) survives as a thin orchestrator that calls a fixed pipeline of resolvers and projects eachResolvedarm into the correctGraphitronFieldvariant. Each cross-cutting concern lifted into its own resolver returning a sealed result, sibling toArgumentRef’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, sealedResolvedwithSuccess.{TableBound, Result, Scalar}/ErrorsLifted/Rejected),TableMethodDirectiveResolver(@tableMethod, two-arm lift,Resolved.{TableBound, NonTableBound, Rejected}gated byisRoot),ExternalFieldDirectiveResolver(@externalField, single-arm lift,Resolved.{Success, Rejected}),LookupKeyDirectiveResolver(@lookupKey, three-arm lift,Resolved.{Ok, Rejected}withresolveAtRoot/resolveAtChild(_, withSplitQuery)entry points). Projection resolvers (lift bundled monoliths into focused units):OrderByResolver(~230 lines, sealedResolved.{Ok, Rejected}owning the canonical@defaultOrderfallback message),LookupMappingResolver(pureprojectForLookupref-walker, total projection so noResolvedwrapper),PaginationResolver(clustersprojectPaginationSpec+isPaginationArg+resolveDefaultFirstValuesince they’re all pagination semantics),ConditionResolver(@conditionresolution, two sealed result typesArgConditionResult/FieldConditionResulteach{None, Ok, Rejected}replacing the prior dual-signal pattern),InputFieldResolver(plain-input-field classification wrappingBuildContext.classifyInputField),MutationInputResolver(DML@mutationinput 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 onFieldBuildersince 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 onFieldBuildersince migrating them toBuildContextwould muddy that class’s schema/jOOQ-classification concern; eleven orphan imports (one model + tenBuildContextstatic imports left behind by the directive- and projection-axis lifts) removed, andfieldArgumentNames’s redundantly fully-qualified `Collectors/LinkedHashSetreferences 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:LookupKeyDirectiveResolverdrops unused(BuildContext, ServiceCatalog, FieldBuilder)constructor params;OrderByResolverconsumes the classifiedArgumentRef.OrderByArgdirectly instead of looking theGraphQLArgumentup 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);MutationInputResolverlifts the@mutation(typeName:)raw String into aDmlKindenum + sealedDmlKindResult.{Absent, Kind, Unknown}(replaces 8+ string-equality comparisons across three sites with exhaustive enum switches and removes an "unreachable: typeName=…" default arm). Net result:FieldBuildershrinks 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 thatArgumentRefset 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 at7bf0303+2635d97+b36f230+9e38cc9+1b07b72+f2ba2c5+995bb29+a923694; phases f-A/f-B/f-C/f-D/f-E at67999cf+d43f1e3+b21a152+8fe072f+3298ac7; phases g-A/g-B/g-C at29734fb+6c54435+e4ac4ee; encoder cleanup at1275396+8a12231; status flip at0b26872; post-review notes at3459225; final retirements atfad83a7; R55 follow-on filed at72ae5cc): wire-format encoding and decoding for@nodeIdids now lives at the DataFetcher boundary; the classifier model and emitted query builders below it see decoded key tuples and standard column predicates rather thanNodeIdEncoder.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.NodeIdDecodeKeyssealed into two arms (SkipMismatchedElementfor filter call-sites where a malformed id short-circuits to "no row matches";ThrowOnMismatchfor lookup-key / mutation-key call-sites where a wrong-type id is a contract violation);CallSiteCompactionsealed root withDirect(plain projection) andNodeIdEncodeKeys(HelperRef.Encode)(encode-on-projection) arms; the third failure mode (NullOnMismatchforQuery.node/Query.nodes/ federated_entities) is dispatcher-driven rather than carrier-driven and lives inEntityFetcherDispatchClassGenerator. Composite-key column carriers: newInputField.CompositeColumnField/CompositeColumnReferenceField,ChildField.CompositeColumnField/CompositeColumnReferenceField,ArgumentRef.ScalarArg.CompositeColumnArgfor arity > 1 NodeIds; arity-1 cases stay on the existing single-column carriers (which gainextraction/compactionslots). TheComposite*variants narrow their boundary slot to the only arm the classifier produces (NodeIdDecodeKeyson input,NodeIdEncodeKeyson output) at the type system level rather than asserting via validator rule.BodyParam.ColumnPredicatesealed sub-taxonomy replaces the oldColumnEq(boolean list)shape with four predicate-arm records:Eq/In(single column) andRowEq/RowIn(composite-key tuples emittingDSL.row(c1, ..., cN).eq(...)/.in(...)). Lookup arg restructure:LookupMapping.ColumnMappingretypes from a flatList<LookupColumn>toList<LookupArg>sealed intoScalarLookupArg(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);InputColumnBindinggeneralises 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.HelperRefnew sealed sibling ofMethodRefwith separateEncode/Decodearms because the sameList<ColumnRef>plays semantically different roles on each side (call-site Java parameter list vs returnedRecordN<...>shape);GraphitronType.NodeTypegains pre-resolvedencodeMethod/decodeMethodfields read by every emitter and the encoder generator from one source of truth.NodeIdEncoderAPI: per-Node-typeencode<TypeName>(...)/decode<TypeName>(String) → RecordN<T1..TN>helpers replace the genericencode("typeId", ...)/decode("typeId", ...)surface;peekTypeId(String)stays as the only generic public method (used by typeId-fanout sites); the genericencode/decodeValuesbodies become private. Deleted:hasIds/hasId(query-builder helpers that did not belong in the encoder),coerceValue(the per-type decoders inlinegetDataType().convert(...)per slot statically),canonicalize(no callers;Base64.getUrlDecoderaccepts 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’skeyColumns) ships classifier-only withFetcherEmitterruntime stubs, deferred to R24’s expanded scope. Multi-hop FK and condition-join correlated-subquery emission stays in R24.MutationField.DmlTableField.nodeIdMetaretypes toencodeReturn: Optional<HelperRef.Encode>so the DML emitter no longer reconstructs the helper reference from a typeId string at emission time;JooqCatalog.NodeIdMetadatasurvives only as a classifier-time intermediate. Validator coverage lands one arm per new sealed variant (twoNodeIdDecodeKeys, twoCallSiteCompaction, fourColumnPredicate, threeLookupArg, twoInputColumnBinding, fiveComposite*carriers);TypeFetcherGenerator.NOT_DISPATCHED_LEAVESshrinks. 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:nodeidfixturegains a composite-PKBarNode type for[ID!] @nodeId(typeName: "Bar")row-IN coverage and a rooted-at-parentparent_node+child_refshape (FK targets non-PK unique column) ready to drive R24’s emitter coverage. Test surface: every@nodeIdexecution test continues to round-trip (Query.node,Query.nodes, federated_entities, same-table filter, rooted-at-child reference, composite-PK lookup); SQL inspection viaExecuteListenerconfirms emitted bodies arec.eq/c.in/DSL.row(...).eq/.inover decoded key tuples rather than encodedStringids;Query.nodesper-typeId batch SQL pinned toVALUES + JOIN + ORDER BY idxshape (regression test catches dispatcher fallback to legacyWHERE row-IN); failure-mode parity verified per arm. Post-review cleanup retired two compat fallbacks (InputField.NodeIdFieldandNodeIdReferenceField’s "classified-but-inert" arms in `BuildContext) that survived the wire-shape variant deletions; both reroute toInputFieldResolution.Unresolvedwith pointedAUTHOR_ERRORreasons. Coupling: R20 (IdReferenceFieldcode 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 collapseEntityFetcherDispatch’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 inretire-synthesis-shims.md(R27) andfaceted-search.md(R13) updated to name the post-R50 column-shaped successors. Stale{@link InputField.IdReferenceField#targetTypeName}javadoc inBuildContextand "R50 phase b2b" stub messages inFetcherEmitter/TypeFetcherGeneratorretexted 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 tographitron-fixturesmain scope; applied to every@Test-bearing class ingraphitron(87 classes) andgraphitron-test(8 classes);GeneratorDeterminismTestcarries@Tag("cross-cutting")directly as the sole cross-cutting test. Enforcement test added per-module: each walks its owntarget/test-classestree and fails the build if any@Test-bearing class lacks exactly one tier identity. Newgraphitron-rewrite/docs/testing.adocwith decision rubric, per-tier sections, module-location vs. tier table, and build commands. Cross-links:rewrite-design-principles.adoctier sections trimmed to one-liner pointers;docs/README.adocDetailed reference list gains the new file;.claude/web-environment.mdgains a one-line pointer. Javadoc sweep replaced "Level N" and mismatched tier prose with tier annotations acrossFieldValidationTestHelper,GraphitronSchemaBuilderTest,GeneratedSourcesSmokeTest,IdempotentWriterTest, andGeneratorDeterminismTest. UnblocksR25 rebalance-test-pyramidwhich depends on the canonical tier names. Review fixes (bb83da6): two brokenxref:links to.claude/web-environment.mdreplaced 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.adoclifted 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 thedirectives.graphqlsinjection-before-classification and orphan-sweep-after-every-emit ordering invariants thatcode-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-referencein 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#4numbering, add a real preamble) was absorbed by R9’s AsciiDoc migration. -
ExternalCodeReference.argMappingfor 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@conditionsite (field-level, argument-level, input-field-level, path-step). The schema gains anargMapping: Stringfield onExternalCodeReferencecarrying a comma-separatedjavaParam: graphqlArgmini-DSL (target-on-left, matching the internalMap<javaTarget, graphqlSource>shape and the@experimental_constructType.selectionconvention). 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 onENUM_VALUE.ArgBindingMapcollapses the previousforField/identityFor*family to one axis-agnostic factoryof(Set<String>, Map<String, String>)returning sealedResult.{Ok, UnknownArgRef}; a new parserparseArgMapping(String)returns sealedParsedArgMapping.{Ok, ParseError}and enforces unique Java targets, with order-preserving iteration viaLinkedHashMap+Collections.unmodifiableMap.FieldBuilder.ExternalRefandBuildContext.ConditionDirectiveretype to carryargMappingandargMappingErrorseparately fromlookupError; failure precedence makeslookupErrorwin overargMappingErrorso "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@tableMethodarms (root + child),buildArgCondition,buildFieldCondition,BuildContext.resolveConditionRef, andbuildInputFieldCondition. Path-step@conditionresolves with an empty slot set, so any non-emptyargMappingrejects throughUnknownArgRef;resolveConditionRefreturns a newConditionResolution(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):parseExternalRefrejectsargMappingon@externalField;TypeBuilder.buildResultTypeandbuildNonTableInputTypereject on@record; the enum-classify branch rejects on@enum. Tests across all upper tiers: newArgBindingMapTest(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);GraphitronSchemaBuilderTestcases 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;ServiceCatalogTestpost-reflection error messages reframed from@field(name: "X")toargMapping entry 'X: Y'. Pipeline + execute coverage:filmsByServiceRenamedfixture (graphitron-test/src/main/resources/graphql/schema.graphqls) authored directly withargMapping: "filmIds: ids";GraphQLQueryTest.queryServiceTable_filmsByServiceRenamed_overrideBindsArgToDifferentlyNamedJavaParamround-trips against PostgreSQL. R41 was tombstoned under the workflow’sDiscardedterminal-state rule; R53 inherited R41’s reviewed design conclusions but ran its own Spec → Ready review cycle. -
@servicerows-method body ; first iteration (service-rows-method-body+ R49 Phase B,befc156): replaces the previously-stubbed body emitted bybuildServiceRowsMethodwith a working call site. The shared emitter now handles bothServiceTableFieldandServiceRecordField, walkingMethodRef.params()to build the developer’s call:ParamSource.Sources→ the loader’skeysparameter (passed through directly; element-shape conversion is a follow-up),ParamSource.DslContext→ adsllocal declared fromgraphitronContext(env).getDslContext(env)when needed,ParamSource.ArgandParamSource.Contextvia the existingbuildArgExtractionandgetContextArgumentpaths.ArgCallEmitter.buildMethodBackedCallArgsgains a 4-arg overload accepting asourcesExpressionCodeBlock; the legacy 3-arg overload delegates withnullso root-level@service(where Sources is rejected at classifier time per Invariants §2) still throws when it sees the variant.buildServiceRowsMethodtakes(BatchKeyField, MethodRef, ReturnTypeRef, perKeyType, parentTypeName, outputPackage); the dispatch site for both service variants threads the variant’sMethodRefthrough.FilmService.titleUppercasefixture switches fromSet<Row1<Integer>>toSet<Record1<Integer>>(classifies asBatchKey.MappedRecordKeyed, framework emitsRecord1keys viaGeneratorUtils’s `RecordKeyedbranch which usesRecord.into(Tables.FILM.FILM_ID)for extraction); body fetches each film’s title from thefilmtable and returnsMap<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>extendsRow1<T>and addsvalue1()for application reading. The framework’s continued emission ofRow1keys for theRowKeyed/MappedRowKeyedBatchKey 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 aSet<Row1<Integer>>dev would. Execution testGraphQLQueryTest.films_titleUppercase_resolvesViaServiceRecordFieldDataLoaderexercises 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, theRow1follow-up, strict-return-type validation againstfield.elementType(), the typed-context-value registry coordination) called out for tracking. -
ServiceRecordFieldPhase A ; DataLoader plumbing for child@servicewith scalar /@record-backed return (service-record-field,b9a6900+87a827d+f9bf585+85974ac+ Phase A close-out): liftsChildField.ServiceRecordFieldout ofTypeFetcherGenerator.NOT_IMPLEMENTED_REASONSintoIMPLEMENTED_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 throwsUnsupportedOperationExceptionat request time. Model:ChildField.ServiceRecordFieldgains a non-nullBatchKey batchKeyfield andimplements MethodBackedField, BatchKeyField;rowsMethodName()follows the sameload<X>convention asServiceTableFieldso the existing dispatch + key-extraction infrastructure picks the variant up viaBatchKeyFieldpattern matching.elementType()accessor closes the deferral noted inset-parent-keys-on-service.mdby deriving the per-key V from the schema directly:ResultReturnTypewith non-nullfqClassName→ backing class;ScalarReturnType→ standard GraphQL scalar’s Java type (String/Boolean/Integer/Double/StringforID; custom scalars and enums fall back toStringuntil the Phase B consumer scalar registry surfaces typed Java classes); other cases fall through to the reflected outer return onMethodRef.returnType(). Builder: Site 1 (classifyChildFieldOnResultType,@record-typed parent) scalar/record-return arms becomeRejectionKind.DEFERREDwith 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 existingextractBatchKey(MethodRef)helper and constructs the variant. Validator:validateServiceRecordFieldrejects non-emptyjoinPathwithRejectionKind.DEFERREDuntil the lift form ships. Generator:buildServiceDataFetcherandbuildServiceRowsMethodare parameterised by(ReturnTypeRef, perKeyType)rather than the previousTableBoundReturnTypehard-coded `+RECORD; `ServiceTableFieldpassesRECORD,ServiceRecordFieldpassesfield.elementType(). Drive-by fixes uncovered by the first child-@serviceschema fixture ingraphitron-test:dfe.getSelectionSet().getField(<name>)(non-existent API onDataFetchingFieldSelectionSet) → drop theselextraction and theselparameter 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 ofelementType()above (the previousRECORDhard-coding meant the generator was correct forServiceTableFieldonly ;ServiceRecordFieldwould have shippedDataLoader<K, Map<K, V>>had the per-key-type lookup not been added in this Phase). Coverage: sixTypeFetcherGeneratorTestcases 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 existingserviceField_*rows-method-signature tests are updated for the droppedselparameter. Builder-tier coverage:GraphitronSchemaBuilderTest.NonTableParentCase.SERVICE_FIELD_ON_RESULT_TYPEflips from "ServiceRecordField" to "DEFERRED with @record-parent reason";ServiceFieldValidationTest.RecordCase.NO_PATHexpects no errors (variant is implemented) andWITH_LIFT_CONDITIONexpects the newjoinPathDEFERREDrejection. 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) plusFilm.titleUppercase: String @service(...)ongraphitron-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 againstfield.elementType()is also Phase B’s deliverable since the structural unwrapping (Map<KeyType, V>vsList<V>) is the same logic Phase B’s body emitter encodes. -
@externalFieldresolved-reference path →ComputedFieldshipped end-to-end (computed-field-with-reference,137f9d2+8ca2c78+650de56+8a6685b): liftsChildField.ComputedFieldout ofTypeFetcherGenerator.NOT_IMPLEMENTED_REASONSintoIMPLEMENTED_LEAVES.@externalFieldgains a mandatoryreference: 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. NewARG_EXTERNAL_FIELD_REF = "reference"constant inBuildContext.ChildField.ComputedFieldgains a non-nullMethodRef methodfield andimplements MethodBackedField; theMethodRef.Basiccarries the captured parameterised return type (Field<X>) as a structuralTypeNameand oneParam.TypedatParamSource.Tablefor the parent table parameter. NewServiceCatalog.reflectExternalField(className, methodName, parentTableClass)mirrorsreflectTableMethodwith a stricter contract: must bepublic static, must take exactly one parameter assignable from the parent’s jOOQTable<?>class, must return parameterisedorg.jooq.Field<X>(rawFieldis rejected).FieldBuilder’s `@externalFieldarm now parses the reference, runs an alias-collision check viaJooqCatalog.findColumn(rejects when the GraphQL field name shadows a real SQL column on the parent@table), reflects the method, and constructsComputedFieldwith a populatedMethodRef; all resolution failures surface asAUTHOR_ERROR.TypeClassGenerator.emitSelectionSwitchgains aComputedFieldarm emittingcase "<name>" -> fields.add(<RefClass>.<method>(table).as("<name>"));to inline the developer’s call into the projection list;build$FieldsMethodandbuildTypeSpectake a newcomputedFieldsparameter so the field actually reaches the switch (the previousflatcollection only includedColumnField/NodeIdField/TableField/LookupTableField/NestingField).FetcherEmitter.dataFetcherValuegains aComputedFieldarm emittingnew ColumnFetcher<>(DSL.field("<name>")), reading by alias from the result Record.TypeFetcherGenerator’s dispatch arm becomes a no-op (wired by `FetcherEmitter, projected byTypeClassGenerator).validateComputedFieldrejects a non-emptyjoinPath(lift form) withRejectionKind.DEFERREDuntil the@referencepath lands. Tests across all upper tiers:GraphitronSchemaBuilderTest.ComputedFieldCaseextendsSCALAR_RETURNto assert the resolvedMethodRefshape (className,methodName, singleParam.Table) plus newMETHOD_NOT_FOUND(reflection failure surfacesAUTHOR_ERRORwith the missing-method name) andNAME_COLLIDES_WITH_COLUMN(alias-collision rejection);ComputedFieldValidationTestNO_PATHflips to expect no errors (variant is now implemented),WITH_LIFT_CONDITIONexpects the newDEFERREDrejection; conflict-test fixtures atGraphitronSchemaBuilderTest:3802/3837updated withreference: {...}so the now-mandatory schema parses. New test fixture classTestExternalFieldStubprovides the reflection target for the schema-builder tests; new fixture classFilmExtensions.isEnglish(Film) -> Field<Boolean>ingraphitron-fixturesprovides the execution-tier target. NewFilm.isEnglish: Boolean @externalField(reference: {className, method})field ongraphitron-test/schema.graphqls; newGraphQLQueryTest.films_isEnglish_resolvesViaExternalFieldExpressionend-to-end against PostgreSQL via-Plocal-db. Docs:code-generation-triggers.mdline 171 reflects the new directive shape and code-emission contract;graphitron-lsp.mdPhase 5 dispatch table extended with@externalFieldreference-argument completion as a tracked deliverable. The legacy no-arg form is not supported; downstream schemas (~49 known instances in Sikt projects) must addreference: { 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 existingGraphitron.buildSchema(...)call. Classify-time model: newEntityResolution(typeName, table, alternatives, nodeTypeId)sidecar onGraphitronSchema.entitiesByType, populated by a newEntityResolutionBuilderthat walks every@key-bearing or@nodetype afterTypeBuilder/FieldBuilder. Each resolution carries one or moreKeyAlternative(requiredFields, columns, resolvable, KeyShape)entries;KeyShape.NODE_IDis synthesised for everyNodeType(decoded viaNodeIdEncoder.decodeValues(typeId, id)at runtime),KeyShape.DIRECTis emitted for consumer-declared@keydirectives (rep field values map index-by-index to column values).@node+ explicit@key(fields: "id", ...)dedups by promoting the consumer’s directive while pinningNODE_IDshape so the dispatcher still decodes throughNodeIdEncoderrather than treating the literal"id"string as a column value; this preserves the documentedresolvable: falseopt-out. NewFederationKeyFieldsParserrejects nested selections, dotted paths, aliases, arguments, variables, comments, and numeric values with targetedParseExceptiondiagnostics;GraphQLSelectionParseris left untouched. Build-time SDL synthesis: newKeyNodeSynthesiserregistry post-step (betweenFederationLinkApplierandTagApplierinloadAttributedRegistry) attaches@key(fields: "id", resolvable: true)to every@nodetype that does not already carry an explicit@key(fields: "id", ...), so the supergraph composer sees the entity declaration. Runtime emission: newEntityFetcherDispatchClassGeneratoremits anEntityFetcherDispatchclass withfetchEntities(env)/resolveByReps(reps, env)/resolveType(env)/typenameForTypeId(typeId). Per-rep flow walks alternatives in most-specific-first order, picks the first resolvable alternative whoserequiredFieldsare all present in the rep, builds a per-rep DFE rebindingargumentsto the rep sogetTenantId(repEnv)resolves against the individual rep, decodes into a column-value row, and groups bindings by(alternative-index, tenantId)into nestedLinkedHashMap`s. Per-group dispatch issues one SELECT per group via a `VALUES (idx, col1, col2, ...) JOIN <table> ORDER BY idxderived table; theidxcolumn carries through SQL soresult[row.idx] = rscatters rows back to original federation positions as a SQL property, not a Java post-processing step. Projection includesinline("Foo").as("__typename")plus<TypeName>.$fields(env.getSelectionSet(), table, env); graphql-java’sDataFetchingFieldSelectionSetis type-scoped at the_entitiesDFE call site, so per-type$fieldswalks pick up only the inline fragment scoped to each__typename(no cross-type batching needed).QueryNodeFetcher.rowsNodesandfetchByIdrewired to synthesise{__typename, id}reps and callresolveByReps; the previous per-typeId loop and its canonicalize-encode-scatter round-trip disappear becauseidxcarried through SQL preserves order directly andBase64.getUrlDecoderaccepts both padded and unpadded forms. Schema wire-up replaces the placeholderfetchEntities/resolveEntityTypelambdas inGraphitronSchemaClassGenerator’s two-arg `build()withEntityFetcherDispatch::fetchEntities/EntityFetcherDispatch::resolveTypewhenentitiesByTypeis non-empty; otherwise the placeholder lambdas stay so a@link-but-no-entity schema still wraps cleanly.AppliedDirectiveEmitter.emitAstLiteralValueswitches from per-scalar enumeration toValuesResolver.valueToLiteral(...) → AstPrinter.printAst → Parser.parseValue, eliminating a class of latent custom-scalar /Float/ input-object / internally-coerced-enum bugs.ColumnRefaddscolumnClass()accessor used by the dispatcher to type the derived-tableRowarity. Federated test fixture (graphitron-test/src/main/resources/graphql/federated-schema.graphqls) is isolated from the sharedschema.graphqlsvia a secondgraphitron-mavenexecution generating intono.sikt.graphitron.generated.federated; non-federation tests keep their previous output package. Test coverage across three tiers: 11EntityResolutionBuilderTestclassify-time cases (NODE_ID synthesis with/without explicit@node(typeId:), DIRECT alternatives, multi-key, dedup, compound,resolvable: falsecarry-through, unresolvable-field demotion, nested-selection rejection, empty-fields rejection, non-@tablerejection); 19FederationKeyFieldsParserTestcases covering naked / braced / mixed whitespace / underscore-and-digit identifiers / commas / nested rejection / unbalanced braces / dotted / aliased / arguments / hash-comments / variables / numeric; 7FederationBuildSmokeTestcases (two-arg shape,_Service+_entitiesfield present,_Entityunion membership,_Service.sdlcarries synthesised@key(fields: "id")on every@nodetype, customizer invocation, one-arg → two-arg delegation); 16FederationEntitiesDispatchTestend-to-end cases against PostgreSQL (single NODE_ID rep, mixed-typename order preservation, empty representations, unknown__typename, garbage NodeId, DIRECT-shape viafilmId, 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@keyyielding null without firing SELECT); plus 2NoFederationRegressionTestcases asserting the shared fixture builds a non-federated schema and emits only the one-argbuildSchemaoverload.getting-started.mdupdated:@linkintro broadened (a baseschema { ... } @linkis also accepted), and the two-arg-form example reframed as an escape hatch for entity types Graphitron does not classify (custom fetchers must return jOOQRecord`s with a `__typenamecolumn for the defaultresolveEntityTypeto recognise them). Hygiene pass shipped alongside:FEDERATION_DIRECTIVE_NAMESmoved behind an initialisation-on-demand holder so federation-jvm load failures only surface on schemas that use federation;buildRecipeErrorsmixed-error trade-off documented; the federation spec URL lifted fromFederationLinkApplier.DEFAULT_FEDERATION_SPEC_URLto a new neutralFederationSpecclass in the federation knowledge package (three callers no longer reach into a pipeline class for a constant); unusedSchemaDirectiveRegistry.FEDERATION_DIRECTIVESandisFederationdeleted (zero production callers; can be brought back if needed);federationLinkthreaded via a newAttributedRegistry(registry, federationLink)carrier returned fromloadAttributedRegistry, soKeyNodeSynthesiserandGraphitronSchemaBuilder.buildBundleno longer re-walk the registry to discover whatFederationLinkApplier.applyalready determined, andFederationLinkApplier.hasFederationLinkdeletes. Non-goals: Federation 1, customresolveEntityTypeextension point,@interfaceObject,TableInterfaceTypeas a federation entity, nested-selection@key, build-time_service.sdlartefact emission, cross-__typenameSQL union batching, cross-field DataLoader sharing into the entity dispatcher. 909 unit + 23 federation tests green. -
Set<T>parent-keys on@servicemethods →MappedBatchLoader(set-parent-keys-on-service,eebf881): extends theBatchKeysealed hierarchy from two to four variants via the cross-product of container axis (Listpositional vsSetmapped) and key-shape axis (RowNvsRecordN):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.classifySourcesTypereplaces itsList.class-only guard with a dualisList/isSetcheck and picks the variant from the two-axis cross-product;Set<TableRecord>classifies asMappedRowKeyed(matching howList<TableRecord>classifies asRowKeyed).dtoSourcesRejectionReasonreceives the same dual check soSet<SomePlainClass>now produces "not backed by a jOOQ TableRecord" instead of falling through to the generic "unrecognized sources type" path.TypeFetcherGenerator.buildServiceDataFetcherpicksnewMappedDataLoadervsnewDataLoaderfrom the variant and types the lambda’skeysparameter asSet<KeyType>vsList<KeyType>accordingly; drive-by fix: the existing positional path was callingDataLoaderFactory.newDataLoaderWithContext(...)which does not exist on the API (the split-query path was already correct withnewDataLoader).buildServiceRowsMethodreturnsMap<KeyType, List<Record>>/Map<KeyType, Record>for mapped variants andList<List<Record>>/List<Record>for positional; the data-fetcher return type staysCompletableFuture<V>in all four cases sinceloader.load(key, env)yields a per-key promise regardless of the underlying batch-loader shape.GeneratorUtils.keyElementTypeandbuildKeyExtractiongroup by shape via multi-pattern arms. Tests:ServiceCatalogTestgains fourreflectServiceMethod_setOf*Sourcesclassification cases (TableRecord, Row1, Record1, DTO-rejection) plus alistOfRecord1regression;TypeFetcherGeneratorTestgainsserviceField_mapped*coverage of the Set/Map shapes and a regression for thenewDataLoaderfix. Unblocks production schemas that declare@servicechild fields withSet<SomeRecord>keys. -
Same-table
[ID!] @nodeIdfilter: primary-key IN predicate (3fdfbfa+19180ea): a[ID!] @nodeId(typeName: T)field on a@tableinput type whoseTresolves to the input’s own table now classifies asInputField.NodeIdInFilterFieldand emitsNodeIdEncoder.hasIds("typeId", arg, table.col1, ..., table.colN), short-circuiting toDSL.noCondition()when the list is null or empty.BuildContext.classifyInputFieldadds a same-table guard beforefindUniqueFkToTable(t, t)(which would always miss for a self-FK lookup) and resolvesnodeTypeId/nodeKeyColumnsvia the same three-tier fallback asNodeIdReferenceField:JooqCatalog.nodeIdMetadatafirst, then post-first-passctx.types, then SDL-only@nodewith the catalog primary key as a last resort.BodyParammigrates from a single record to a sealed interface withColumnEq(existing scalar/IN path) andNodeIdIn(new) variants;TypeConditionsGenerator.buildConditionMethodswitches on the variant and now takesoutputPackageso it can fully-qualify the generatedNodeIdEncoderreference.walkInputFieldConditionsinFieldBuilderemitsBodyParam.NodeIdInfor the new leaf, gated bylookupBoundNamesso a future@lookupKey-bound combination still routes throughLookupMapping.NodeIdMappinginstead.ArgCallEmitter.buildNestedInputFieldExtractionnow wraps the leaf cast inList<...>whenparam.list()is true, fixing the call-site cast for list-shaped filter input fields (the spec assumed this already worked).TypeFetcherGenerator.NOT_DISPATCHED_LEAVESandGraphitronSchemaValidatorregister the new variant. Tests across three tiers:NodeIdPipelineTest.InputSameTableNodeIdCase(composite-PK, single-PK, target-not-@nodeUnresolved) using thenodeidfixturecatalog because the same-table case requires__NODE_KEY_COLUMNSmetadata that Sakila tables lack;TypeConditionsGeneratorTest(single-column, composite-column, list-of-String parameter type, mixedColumnEq+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 tonoCondition()returning all rows.VariantCoverageTest.NO_CASE_REQUIREDcarries an entry pointing at the pipeline test, parallel to howNodeIdFieldandNodeIdReferenceFieldare already handled. Cleanup pass dropped a deadnonNullfield onBodyParam.NodeIdIn(the body always guardsarg == null || arg.isEmpty()so outer-list nullability is moot). -
Auto-emit Relay
nodes(ids:)resolver whennode(id:)exists (auto-nodes-relay-resolver,71e439f+aa33bd3+cbbc103+40e22b2+44d0201+6b865f3+4aa79f7): newQueryField.QueryNodesFieldsealed variant routed byFieldBuilder.classifyQueryFieldfor any root-query field namednodesreturning[Node]/[Node!]/[Node]!/[Node!]!;GraphitronSchemaValidatoradds a no-op arm andTypeFetcherGenerator.buildQueryNodesFetcheremits a thin delegator toQueryNodeFetcher.getNodesparallel to the existingbuildQueryNodeFetcher.QueryNodeFetcherClassGeneratorextracts the per-typeIddispatch out ofgetNodeinto a privatefetchById(env, id)helper reused by both single- and batch-paths, then adds agetNodesmethod that fansidsinto per-tenantDataLoader<String, Record>`s keyed by `getTenantId(idEnv) + "/" + path, whereidEnvis a per-idDataFetchingEnvironmentImpl.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 onehasIdsquery 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 callbackrowsNodes(keys, env)groups keys bypeekTypeId, runs onedsl.select(...).from(t).where(NodeIdEncoder.hasIds(typeId, typeIds, keyCols)).fetch()per typeId, and scatters rows back to original positions via aMap<String, List<Integer>>keyed byNodeIdEncoder.canonicalize(peekTypeId(id), id)so non-canonical inputs (padded base64, the URL decoder accepts trailing=whileencode()emits the no-padding form) still match the canonical encoded id from the result row, eliminating a silent disagreement withnode(id:). NewNodeIdEncoder.canonicalize(typeId, base64Id)(decode + re-encode, null on malformed input or typeId mismatch) lives next topeekTypeId. The result-scatter projection always appends eachnodeKeycolumn to the$fieldslist (gated byif (!fields.contains(t.<col>))to dedup against$fields’s `id-driven addition; mirrorsTypeClassGenerator’s required-projection-columns pattern) and a synthetic `__typenamecolumn so the existingNodeTypeResolverstill routes by name.GraphitronContext.getTenantIdjavadoc tightened to spell out the tenant/DSLContextpartition contract: whengetDslContextvaries per id,getTenantIdMUST partition by the same key, since the loader picks oneDSLContextfromkeyContextsList().get(0)for the entire batch. Generator-side comment indispatchNodesdocuments 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_FIELDclassification case,QueryNodeFieldValidationTestno-op case, and 10 execution-tierGraphQLQueryTestcases under "Query.nodes ; Relay batch dispatch" (empty / mixed-type / garbage / unknown-typeId / missing-row / padded-base64 canonicalize regression / duplicate-ids / single-tenantQUERY_COUNT == 2/ per-tenant fan-outQUERY_COUNT == 2/ id-and-other-fields-together asserting both the responseidfield and the rowsNodes encode read from the same key column). 854 unit + 154 execution tests green. -
IdReferenceFieldclassifier + synthesis shim (20b3465+afc11bc+7fc28fe+a313040+c594f0a+37f01fc+ad6303b): newInputField.IdReferenceFieldsealed variant carriestargetTypeName/fkName/qualifier/synthesizeddescribing a filter predicate that resolves to ahas<Qualifier>(s)method on the FK source’s jOOQ record class ; the shapeKjerneJooqGeneratoremits from a single FK out of the input’s resolved table.BuildContext.classifyInputFieldgains two arms between the existing scalar@nodeIdbranch and the@referencebranch: the canonical form ([ID!] @nodeId(typeName: T)with optional@reference(path: [{key:}])when the FK is ambiguous) resolves the FK viaJooqCatalog.findUniqueFkToTable(new) or the explicit@referencekey and emitsIdReferenceFieldwithsynthesized=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 throughJooqCatalog.buildQualifierMap(new ; three lowercase keys per FK: raw qualifier, lowerCamel qualifier, plural lowerCamel qualifier; cached per source table) and synthesizesIdReferenceFieldwithsynthesized=trueplus a per-site WARN whose message namesparentTypeName.fieldNameand the canonical@nodeId(typeName:) [@reference(path: [{key:}])]replacement that future migration tooling can parse out of build logs. Shim gate iscatalog.nodeIdMetadata(targetTable).isPresent(); the same KjerneJooqGenerator-project sentinel that gates the scalarNodeIdFieldshim. New catalog helpers:findUniqueFkToTable,buildQualifierMap,qualifierForFk, plus the package-privatelocalGetQualifierreproduction ofKjerneJooqGenerator.getQualifier(UpperCamelCase fromrole + targetTable + "_id";generateRoleNamereturns"HAR"when source column equals target column, otherwise the role discriminator). Newidreffixtureschema (studieprogram + studierett, two FKs: HAR-role onstudieprogram_id, role-prefixedregistrar_studieprogramwhose qualifierRegistrarStudieprogramStudieprogramIddeliberately does not match any source column) wired throughNodeIdFixtureGenerator.METADATAso the targetstudieprogramcarries__NODE_TYPE_ID. Tests across three tiers:JooqCatalogIdRefTest(22 cases on Sakila + nodeidfixture + idreffixture forfindUniqueFkToTable/buildQualifierMap/qualifierForFk/generateRoleName),IdReferenceShimClassificationTest(5 cases ; explicit@field(name:), bare plural, bare scalar, bareid: IDfalls through toNodeIdField, role-prefixed where map key ≠any source column),IdReferenceShimWarnFormatTest(4 cases ;parentType.fieldNameformat, FK1 + FK2 ambiguous canonical replacements both include@reference, single-FK unique replacement omits@reference), plusGraphitronSchemaBuilderTest.TableInputTypeCasecases on Sakila for canonical-form coverage and matchingNodeIdPipelineTestupdates.TypeFetcherGenerator.NOT_DISPATCHED_LEAVESregisters the new variant; code generation lifts in a follow-up tracked atroadmap/id-reference-input-field.md(Spec). 853 unit tests green. -
BatchKey.ObjectBasedremoved (batchkey-remove-objectbased): collapses theBatchKeysealed hierarchy to two variants (RowKeyed,RecordKeyed).ServiceCatalog.classifySourcesTypesplits the former singleClass<?>arm:TableRecord<?>element types now classify asRowKeyedfrom the parent table’s PK columns via a newSourcesClassificationsealed result type; non-TableRecordelement types returnDtoSourcesUnsupportedand surface asUnclassifiedFieldwith an error message naming the field, the sources parameter type, and thebatchkey-lifter-directive.mdbacklog item.GeneratorUtils.keyElementTypeandbuildKeyExtractionObjectBasedswitch arms deleted; both switches are now exhaustive over two variants.GraphitronSchemaValidator.validateServiceTableFieldObjectBasedescape hatch (hasRowOrRecordKeyedearly-return) deleted; the parent-table-PK check runs unconditionally. Test coverage:ServiceCatalogTest.tableRecordSources_classifiedAsRowKeyedanddtoSources_rejectedWithLifterDirectiveHint(classifier unit);ServiceFieldValidationTest.OBJECT_BASEDrewritten asDTO_SOURCES_REJECTEDasserting the rejection path; one pipeline case for the end-to-end DTO rejection. 747 unit tests green; fullmvn install -Plocal-dbclean. -
Interface fetchers: selection-set-aware projection (
3b982fc): replaces the unconditionaltable.asterisk()inbuildQueryTableInterfaceFieldFetcherandbuildTableInterfaceFieldFetcherwith a runtime-builtLinkedHashSet<Field<?>> fieldspopulated with the discriminator column first (always, regardless of selection set) followed byaddAll(<Participant>.$fields(env.getSelectionSet(), table, env))perParticipantRef.TableBound. The set deduplicates shared columns (e.g.titledeclared on bothFilmContentandShortContentcollapses 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. Newparticipants: List<ParticipantRef>component onQueryField.QueryTableInterfaceFieldandChildField.TableInterfaceFieldrecords, threaded byFieldBuilderfromTableInterfaceType.participants()at classification time. NewTypeFetcherGenerator.buildInterfaceFieldsListhelper isolates the field-list emission from both fetcher variants. Six newTypeFetcherGeneratorTestcases (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.ratingvia JOIN tofilm) carved out asinterface-cross-table-participant-fields.md; that follow-up will add the conditional LEFT JOIN gated onenv.getSelectionSet().contains("TypeName/fieldName")plus the fixture additions (short_descriptiononcontent,ratingonFilmContent) needed to write its execution-tier tests. -
runtime-extension-points.mdrewritten for the rewrite runtime (13bbbb3+72dda8c): replaced the legacygraphitron-commondescription with the rewrite-emitted contract. The doc now opens with the per-app interface emitted under<outputPackage>.schema.GraphitronContextbyGraphitronContextInterfaceGenerator, lists the three actual methods (getDslContext,getContextArgument,getTenantId), and shows the typed-key registration shape (b.put(GraphitronContext.class, ctx)) and the helperenv.getGraphQlContext().get(GraphitronContext.class)fromTypeFetcherGenerator.buildGraphitronContextHelper. NewgetTenantIdsection documents the previously-undocumented contract that Graphitron concatenatesgetTenantId(env) + "/" + pathto build DataLoader registry keys (perTypeFetcherGenerator.buildDataLoaderName); only the tenant prefix is pluggable, the path component is Graphitron-controlled. New "Where each concern belongs" paragraph compares jOOQConfiguration(cross-cutting),getDslContext(per-request), and schema directives (SDL business semantics), absorbing the scope of the deletedgraphitroncontext-extension-point-docs.mdBacklog item. Wiring example lifted to a pointer atgetting-started.md’s Hello World / Tenant-scoped `DSLContext/ JWT-claim-context-arguments sections. "Complementary Technologies" coverage of jOOQConfiguration,ExecuteListener, and PostgreSQL RLS preserved. "See also" no longer points atgraphitron-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-testkeeps its<release>17</release>output ratchet (the gap it now covers is "Java-18+ syntax leak" rather than "Java-21+"). Reviewer reproduced: full reactormvn install -Plocal-dbon JDK 25 (BUILD SUCCESS, all modules green);mvn -N validateon JDK 21 fails fast atenforce-versionswith "Detected JDK … is version 21.0.10 which is not in the allowed range [25,)";graphitron-lsp.mdPhase 6 no longer owns the bump. -
@asConnectiontotalCountfield (b18b6a0+6fdd231): synthesised Connection types now carrytotalCount: Int(nullable).ConnectionResultgainstableandconditionfields populated by the connection fetcher; the existing 2-arg(result, page)convenience constructor threadsnull, nullfor the Split-Connection path, and a new 4-arg(result, page, table, condition)constructor is called bybuildQueryConnectionFetcher.ConnectionHelperClassGeneratoremits agraphitronContextshim (mirroring the per-fetcher convention) and atotalCount(DataFetchingEnvironment)static resolver that runsdsl.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 omittotalCount.FetcherRegistrationsEmitter.connectionBodyregisters thetotalCountcoordinate gated onconnectionType.schemaType().getFieldDefinition("totalCount"), so synthesised connections always wire it and structural connections wire it only when the SDL author declared the field; the incidentalconnectionTypeMapprojection and unusedConnectionWiringrecord were removed in the same pass.GraphitronSchemaValidator.validateConnectionTyperejects structuraltotalCountfields whose unwrapped type is notGraphQLInt, using the field’sSourceLocation(falling back to the type location for programmatic schemas) so watch-mode and IDE diagnostics highlight the exact line. Pipeline coverage:GraphitronSchemaBuilderTest.ConnectionTypeCasecasesDIRECTIVE_DRIVEN_MINIMAL(synthesised carries nullableInt),STRUCTURAL_CONNECTION(nullwhen absent),STRUCTURAL_CONNECTION_WITH_TOTALCOUNT(structural field preserved);ConnectionRegistrationsTest(synthesised registers, structural-with-Int registers, structural-without does not);ConnectionTypeValidationTest(6 cases coveringInt,Int!, absent,String,[Int!]). Execution coverage inGraphQLQueryTest: filtered count equals row-predicate count, synthesised connection count, noselect countSQL when field not selected (verified via a jOOQExecuteListenerthat records rendered statements), count SQL issued exactly once when selected. Two Backlog follow-ups filed alongside:totalCountfor nested/Split-Connection carriers (returnsnulluntil that wiring ships), and count-only execution path (skip page query when onlytotalCountis selected). -
@notGenerateddirective removed from the supported set:FieldBuilder.classifyFieldshort-circuits any application toUnclassifiedFieldwith reason "`@notGenerated` is no longer supported. Remove the directive; fields must be fully described by the schema." The check runs beforedetectChildFieldConflictso co-occurring directives don’t shadow the no-longer-supported reason. The directive definition stays indirectives.graphqlsonly so the GraphQL parser doesn’t fail withunknown directivebefore we emit our error. TheNotGeneratedFieldsealed leaf, its validator dispatch, and theNotGeneratedFieldfilters inTypeFetcherGenerator/FetcherRegistrationsEmitterare deleted. Input-field paths surface the same rejection:BuildContext.classifyInputFieldshort-circuits toInputFieldResolution.Unresolved, which propagates throughTypeBuilder.buildTableInputTypeand the nested-input recursion as anUnclassifiedTypereason;FieldBuilder.classifyArgumentpre-walks plain-input arg types and emitsArgumentRef.UnclassifiedArgso the surrounding query field becomesUnclassifiedField(necessary becauseprojectFiltersonly surfaces per-field errors when a@condition/@lookupKeygate fires, so the previously-attemptedcondErrorsentry was dead code). Silent-skip filters inTypeBuilder.buildInputTypeand the nested-input branch ofBuildContext.classifyInputFieldare removed. Tests updated:NotGeneratedFieldValidationTestdeleted;GraphitronSchemaBuilderTest.NotGeneratedFieldCasecollapsed into aNOT_GENERATED_DIRECTIVE_REJECTEDentry underUnclassifiedFieldCase;NOT_GENERATED_AND_SERVICE_CONFLICT(now subsumed by the short-circuit) deleted; newNOT_GENERATED_REJECTED_PLAIN_INPUT_ARGcase underUnclassifiedFieldCaseandNOT_GENERATED_REJECTED_TABLE_INPUT/NOT_GENERATED_REJECTED_NESTED_INPUTcases underTableInputTypeCasecover the input-field paths;notGeneratedField_isExcluded/fieldsMethod_excludesNotGeneratedFieldsdeleted (their schemas no longer build). 736 rewrite unit tests green. -
graphitron-rewrite:watchgoal (8ae55b1+6bb5419+ review-fix): newWatchMojoingraphitron-rewrite-mavenre-runs the rewrite generator on.graphqlschanges; 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 aSchemaWatcherevent loop.SchemaWatcherwalks each root recursively at startup and registers newly-created subdirectories on the fly; theMap<WatchKey, Path> registryisConcurrentHashMapso the watch-loop thread (writes fromdispatchonENTRY_CREATE-for-directory) and the debounce thread (writes fromaddRooton re-expanded<schemaInputs>) both touch it safely. Triggers route through aDebounceExecutor(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-arggetLog().error(msg, throwable)form on both the initial run and watch-loop catch path; the loop survives. JVM shutdown hook closes theWatchServiceand debounce executor cleanly. Tests atgraphitron-rewrite-maven/src/test/java/no/sikt/graphitron/rewrite/maven/watch/: 8SchemaWatcherTestcases (write, modify, delete, debounce coalescing, non-.graphqlsfilter, recursive subdirectory registration,OVERFLOWdispatch,addRoot-vs-dispatchregistry race) and 2DebounceExecutorTestcases (burst-coalesces-to-one,closecancels pending). Documentation: new### Watch modesubsection ingraphitron-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,QueryServiceRecordFieldlift out ofTypeFetcherGenerator.NOT_IMPLEMENTED_REASONSintoIMPLEMENTED_LEAVES. NewArgCallEmitter.buildMethodBackedCallArgs(MethodRef, CodeBlock, String)walksMethodRef.params()in declaration order with per-ParamSourceemission (Argvia the existing extraction switch,ContextviagetContextArgument,DslContextas literaldsl,Tableas the suppliedTables.FOOexpression;SourcesandSourceTablethrowIllegalStateExceptionsince the classifier prevents them from reaching the emitter at root). Three newTypeFetcherGeneratorper-leaf emitters:buildQueryTableMethodFetcherdeclares a specific-table local with no cast and projects via<Type>.$fields(...), whilebuildServiceFetcherCommon(shared betweenbuildQueryServiceTableFetcherandbuildQueryServiceRecordFetcher) emits an optionaldsllocal plus a directreturn 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):@servicedirectives now classify instance methods on(DSLContext)holders, restoring legacy parity. The static/instance fork lives onMethodRef.CallShape; emitter dispatches viaserviceCallTarget.MethodRefis sealed with permitsNonCondition(which permitsService/StaticOnly) andConditionFilter.reflectTableMethodcarries a positiveModifier.isStaticrejection paired withservice-catalog-tablemethod-must-be-static@LoadBearingClassifierCheck. Out-of-band:ServiceHolderFactoryextension point not added ; see runtime-extension-points.adoc. -
R5 (
0480a6bd): cleanup-and-hardening pass on the already-shipped composite-key@lookupKeypath.LookupMapping.MapInput/DecodedRecordcanonical constructors reject empty bindings; three new@LoadBearingClassifierCheckkeys (lookup-mapping-bindings-table-coherent,lookup-key-input-field-non-list,lookup-field-non-empty-args) cover the lookup pipeline with matching@DependsOnClassifierCheckconsumers onLookupValuesJoinEmitter. NewLookupMappingTestpins the type-level invariants;LookupTableFieldPipelineTestextended to assert projectedColumnMapping.MapInputshape; newCompositeKeyLookupQueryTestexecution-tier asserts the rendered SQL usesusing ("film_id", "actor_id")so single-column regressions surface in test rather than at runtime. The shipped shape isTableInputArg+MapInput, not theArgumentRef.CompositeLookupArgthe 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): ReshapeBatchKeyintoSourceKey+ unify the rows-method seam. TenBatchKeypermits collapse toSourceKey(flat record carryingtarget,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). Threesource-key.*@LoadBearingClassifierCheckkeys (SourceRowsCall⇒Row, AccessorCall⇒Record, ServiceTableRecord target-aligned⇒empty path) paired with consumers inGeneratorUtils+SplitRowsMethodEmitter.UnifiedEmissionPinsTestpins the three-fetcher / four-rows-method routing structurally. Net type-identity count: from 10+ permits down to 1SourceKey+ 5Readersub-permits + 1LoaderRegistration. The "Sealed hierarchies over enums" worked example lifted out of the principles doc to a new sibling pagegraphitron-rewrite/docs/dispatch-axes.adoc. R75’sResultRowWalkReader 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 +devgoal. Replaces the Rustgraphitron-lspand the legacygraphitron-maven-plugin:introspectJSON producer with a Java LSP module undergraphitron-rewrite/graphitron-lsp, served by the singlemvn graphitron:devgoal binding127.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-processGraphQLRewriteGenerator.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/@recordautocomplete + hover + diagnostics off a JDK 25java.lang.classfile-driven scan including the Phase 5d directive-shape correction (descend through the outerservice:/condition:/record:arg into the nestedExternalCodeReference) and Phase 5e multi-module reactor visibility (MavenSession.getAllProjects()→ every reactor’s compile-output directory;RewriteContext.classpathRootsfield + six-arg back-compat overload), and-parameters-missing detection viaParameter.name == null(Phase 5a–5e), and the bonede→jtreesitter binding swap with vendoredtree-sitter-graphqlgrammar source plus per-platform native build (Phase 6). Phase 7 (Rust archival,IntrospectMojodeletion, consumer migration docs) carved out into R91; Javadoc surfacing + per-line definitions +@externalFieldwalk +argMappingautocomplete deferred into R90; multi-platform native CI tracked under R89. 90+ LSP + 48 graphitron-maven module tests green; full reactor build green includinggraphitron-sakila-exampleagainst the new instance-@servicefixture from R87. -
R134 (
36122dc+7fadbda): Fix mutation empty-input short-circuit to usenewRecordfor single-record payloads.TypeFetcherGenerator.buildMutationDmlRecordFetcherbranches the empty-list arm ondataIsList: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 ondataIsListviafetch()/fetchOne(); this aligns the empty arm. Regression coverage is owned by the compilation tier:graphitron-sakila-example/schema.graphqlsdeclarescreateFilmsPayload(in: [FilmCreateInput!]!): FilmPayload @mutation(typeName: INSERT), the exact bulk-input + single-payload shape that triggered the bug. The generatedMutationFetchers.createFilmsPayloademitsRecord1<Integer> payload = DSL.using(dsl.configuration()).newRecord(Tables.FILM.FILM_ID)and is compiled against real jOOQ classes; a regression tonewResult(...)would re-emitResult<Record>into aRecord1<Integer>local and fail compilation. Scoped to INSERT because bulk UPDATE/UPSERT onMutationDmlRecordFieldstill throw upstream. Follow-up worth filing separately: the non-empty branch on bulk-input + single-record-payload calls.fetchOne()against multi-rowvaluesOfRows(...)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+ reworkd4824e1): ServiceMethodCall walker carrier across the four root sync@servicepermits (QueryServiceTableField,QueryServiceRecordField,MutationServiceTableField,MutationServiceRecordField). Each permit dropsMethodRef method/MethodBackedFieldand gains aServiceMethodCall serviceMethodCallslot via the newServiceFieldsibling interface;buildServiceFetcherCommondrives generation throughServiceMethodCallEmitter. Lands the walker-carrier plumbing every subsequent slice inherits:WalkerResult<C>sealed wrapper, theServiceMethodCallErrorsub-seal ofAuthorError, graphitron-sideDiagnostic/Severitywith the LSP projector at the wire boundary, and the orchestrator’s collect-Err-exclude-field flow (ValidationReport.walkerDiagnostics).ContextArgumentClassifiergrows aServiceFieldharvest arm;ConflictSite.sitewidens to a two-arm sealed identifier. Shipped as a translator over a resolvedMethodRef.Servicerather 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/@defaultOrderviaFieldSort.direction. Lifts the whole-specOrderBySpec.Fixed.direction: Stringdown onto a per-entry typedColumnOrderEntry.direction: SortDirection(nested enum carryingjooqMethodName()+flipped()), so a single fixed spec expresses heterogeneous order (rental_rate DESC, title ASC).OrderByResolverreads the directive-leveldirection:default (ASC when absent on@defaultOrder, ASC for@order), pushes it down per-entry with per-fieldFieldSort.direction:winning, and precomputesFixed.uniformAsconce; PK-fallback andindex:/primaryKey:synthesised entries stampSortDirection.ASCexplicitly (fork b). Emission at every fixed-spec call site (TypeFetcherGenerator,InlineTableFieldEmitter,SplitRowsMethodEmitter) switchesfixed.jooqMethodName()→col.direction().jooqMethodName(). The@orderByhelper bodies dispatch onuniformAsc: uniform-ASC keeps the runtimedir-flips behaviour, mixed is direction-locked (per-entry directions emitted verbatim, runtimedirignored) ; the opt-out semantics over multiplier, per Stability through simplicity + Generation-thinking. Schema change is purely additive (direction: SortDirectiononFieldSort, 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: unitOrderBySpecSortDirectionTest; pipelineGraphitronSchemaBuilderTest(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); executionfilmsByRateDescTitleAsc_executesHeterogeneousOrder+filmsOrderedConnection_mixedOrderEnumValue_ignoresRuntimeDirection(both ordering onrental_rate DESC, title ASCso the DESC primary and ASC secondary-tiebreak are independently observed).6fd3878switched the demonstrators off the seed-uniformrelease_yearontorental_rateso the primary DESC is no longer a no-op tie. Full reactor green. -
R258 (
ac1eee4): Payload-returning UPDATE onto theUpdateRowscarrier. The payload-return UPDATE shapes (updateFilmPayload/updateFilmsPayload) now classify as the newMutationField.MutationUpdatePayloadField/MutationBulkUpdatePayloadFieldleaves (bothUpdateRowsField, non-OptionalInputArgRef+UpdateRowsslots, noDmlKinddiscriminator) throughFieldBuilder.classifyUpdatePayloadField→UpdateRowsWalker(PK-or-UK matched-key membership), neverMutationInputResolver.resolveInput’s `@valuepartition. 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 liveDmlKindrange by rejecting UPDATE in their compact constructors (MutationDmlRecordField→ {INSERT, UPSERT, DELETE},MutationBulkDmlRecordField→ {INSERT, DELETE});MutationInputResolver’s UPDATE `@valueblock becomes a loudIllegalStateException(classifier guarantee made loud). Emit:TypeFetcherGeneratorextracts sharedbuildSingleRecordTwoStepFetcher/buildBulkRecordTwoStepFetcherskeletons parameterized on a carrier-source chain seam (chainFn/perRowBodyFn, not a re-switchedDmlKind);buildCarrierUpdateChainSingle/buildCarrierBulkPerRowUpdateBodysource SET/WHERE fromsetGroupsOf(updateRows().setColumns())/keyGroupsOf(updateRows().keyColumns()), nevertia.setFields(). The payload data field’sSingleRecordTableFieldclassification is grounded by the SDL-coordinate-keyedDmlEmittedbinding (RecordBindingResolver.groundDmlMutationField+ theclassifyChildFieldOnResultTypeunified pass), so no per-fieldreclassifyis needed on the non-DELETE path.FilmUpdateInputdrops@value(shared across all four UPDATE mutations). Coverage:SingleRecordPayloadPipelineTestUPDATE arm split onto the new leaves (@valuedropped from the UPDATEinputBody, UPSERT kept for R188);GraphitronSchemaBuilderTesttruth-table +@condition/UpdateRowsErrorpayload-rejection +DmlRecordprojection cases; execution round-tripsupdateFilmPayload_updatesRowAndReturnsPayloadWithSingleDataField+bulkUpdateWithThreeRowsInNonPkOrderPreservesInputOrderInResponsegreen after the@valuedrop. Full reactor green. Unblocks R188. -
R265 (
8586cbd+53d3da2): Fix non-compilingnew GraphqlErrorException(String)in the NodeIdThrowOnMismatchdecode helpers. Both throw arms inCompositeDecodeHelperRegistry.buildHelper(scalar:131, reachable; list:113, defensive/unreachable today) switch from the non-existent String constructor to the builder formGraphqlErrorException.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 ofArgCallEmitterinto the registry; it survived because the existingCompositeDecodeHelperRegistryTeststring assertions pin only the FQNgraphql.GraphqlErrorException, which renders identically in broken and builder form, and no fixture drives aThrowOnMismatcharm throughjavac. 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:284verified as a correctUnsupportedOperationExceptionsite, not a sixthGraphqlErrorExceptionsite. No new fixture or assertion; the existing unit pins stay green against the builder form. -
R244 (
622a470+0db48be+999b86e+845fc25+95ae80a+ rework18d46d4): Error-channel slice 1, the typedOutcome<T>transport for root@serviceoutcome fields. Retires the developer-payloadconstructionpath (ErrorChannel.PayloadClass, thePayloadConstructionShapefamily,payloadFactory*/declareEarlyPayload*,ErrorRouter.dispatch) for the four root@servicevariants, replacing it with a request-timeOutcomesource the fetcher returns:Success(value)on the happy path,ErrorList(errors)on the mapped-error path, with the unmapped fallback stillErrorRouter.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 generatedOutcomeruntime type (OutcomeClassGenerator), theOutcomeTypeclassification,ErrorChannel.Mapped, the output-walkingErrorChannelWalker(R238’s analogue, absorbing the channel-rule + accessor-coverage checks),ChannelCatchArmEmitter/ChannelEarlyReturnEmitter,ChildField.ErrorsField.Transport.WrapperArm, theErrorChannelWalkerErrorsub-seal ofAuthorError(four arms + LSPgraphitron.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 againstsuccess.value()(delegation prototype rejected by principles review). The wire shape is pinned by theGraphQLQueryTestexecution round-trip (success + mapped-error + unmapped arms), with both rejection rules covered byErrorChannelClassificationTest.nonNullableSuccessProjectionField_rejectsCarrierandOutcomeTypeValidationTest.@tableMethod+ child@serviceflips and the fullPayloadClassdelete are deferred to a follow-up slice (thePayloadClassarm stays live for those paths); DML stays on the sentinel/localContexttransport (R268 owns the arm-switch machinery retirement; R274 the vestigialOutcomeType.successProjection). Supersedes R241, moots R201. Full pipeline green across all tiers.- R268 (fc365b4+ self-reviewf4ae047): Collapse theOutcomearm-switch to a binary fork over reused field resolution. R244 slice 1 introduced a second switch over theChildFieldtaxonomy:FetcherEmitter.armSwitchValueExprre-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 latentIllegalStateException(four allow-listed-but-unimplemented variants) and a false author-error rejection of@table-bound DataLoader data fields (RecordTableField, theopptak-subgraphshape) 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 fromenv.getSource()tosuccess.value(). Five seams:FetcherEmitter.dataFetcherValueforks three structural roles undersourceIsOutcome(errors field + method-backed DataLoader fields fall through to the raw method reference; inline-resolved data fields arm-switch in place viaarmSwitchedInlineDataFetcher/inlineSuccessRead, covering jOOQ-record columnget, the sharedrecordBackedAccessorRead, and constructor/nesting passthrough);GeneratorUtils.buildRecordParentKeyExtraction+ thebuildFkRowKey/buildLifterRowKey/buildAccessorKey*helpers take a source-bindingCodeBlock;DataLoaderFetcherEmitter.buildgains a pre-registration-prelude overload so the@tablefetcher narrowsSuccessand returnscompletedFuture(null)on theErrorListarm before loader registration (the preferred seam-3 ordering);hasWrapperArmErrorshoisted to one home onFetcherEmitterfor bothFetcherRegistrationsEmitterandTypeFetcherGenerator; andGraphitronSchemaValidator.validateOutcomeChildArmSwitchdeletes the allow-list, replacing membership with a contextual structural invariant (every immediate child of aWrapperArmoutcome type resolves through a graphitron-emitted fetcher, never graphql-java’s defaultPropertyDataFetcher) keyed off the sharedFetcherEmitter.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-tierFetcherPipelineTest.outcomePayload_tableDataField_*+outcomePayload_columnDataField_armSwitchesInlineReadOnSuccessValue(classification + wiring kind, no body-string assertions); validation-tierOutcomeTypeValidationTest.outcomePayloadWithTableDataField_isNotRejected(false-rejection fix with classification preconditions); execution-tierGraphQLQueryTest.submitFilmReviewWithFilm_*(new sakilaFilmReviewWithFilmPayloadfixture round-tripping both arms). Full reactor green. -
R283 (
f55b9ac): Emit the@oneOfdirective definition into the federation SDL outputs.ServiceSDLPrinter.generateServiceSDLV2prints the@oneOfapplication but strips the spec-built-in definition (DirectiveInfo.isGraphqlSpecifiedDirective), so Apollo composition rejected the subgraph withUnknown directive "@oneOf". Reinstated on both federation seams from one source of truth: codegen-sideOneOfDirectiveSdl(DEFINITION/usesOneOf/augment) wraps the file arm’sgenerateServiceSDLV2output inSchemaSdlEmitter.printFederationServiceSdl; the generated<outputPackage>.util.OneOfDirectiveSdl(OneOfDirectiveSdlGenerator, wired intoGraphQLRewriteGeneratorunder afederationLink && usesOneOfgate so a non-federation@oneOfschema emits no dead helper) corrects the runtime_Service.sdlby re-printing, appending the definition, and reinstalling aStaticDataFetcher, withGraphitronSchemaClassGeneratorwrapping the federationbuild’s return in `withOneOfDefinition(insideif (federationLink)) under theusesOneOfgate. The one drift-prone string,DEFINITION, is single-sourced from the codegen constant; non-@oneOfschemas keep byte-identical output.@oneOffixture (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 thefederationLinkconjunct). R253 cross-reference note added so its controlled-printer route preserves the@oneOfcarve-out. Full reactor green. -
R149 (
609dc70): End-to-end producer-side test thatGraphQLRewriteGenerator.buildOutput()populates both halves ofBuildOutput.report(). NewBuildOutputReportPipelineTest(@PipelineTier) drives a hand-written schema against the test jOOQ catalog carrying two independent diagnostics, an unresolvable@referencekey (→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 asBuildTriggerPublishesDiagnosticsTest. NOTE: the R149 work landed under the mislabeled commit609dc70("R266 rework: UK-delete execution proof…"), a rebase artifact that also carries the wrong session trailer;git show 609dc70 --statshows only the three R149 files (the test, the narrowed spec, the README row). Full reactor green. -
R285 (
58a7c8f+d7d2861): Lift-back projection for child@servicefields returning a table-bound type (ChildField.ServiceTableField). AServiceTableFieldwas emitted as a terminal record producer (buildServiceRowsMethodreturning the service result verbatim), so non-column sub-fields on the returned type, the first being a@referencecorrelated multiset, failed at query time withField "<name>" is not contained in row type. The fix routesServiceTableFieldthrough the newSplitRowsMethodEmitter.buildServiceTableLiftrows-method: call the@servicemethod, extract each returned record’s PK, re-project the bound table on that PK by identity throughType.$fields(...), carryparentIdx+seqfor scatter and intra-parent order, and re-wrap into the loader container (List/Map × list/single). The loader value type becomes the projectedorg.jooq.Record, not the developer-returnedXRecord. This is the condensedServiceRecordField -> RecordTableFieldshape 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). AllServiceTableField`s now lift uniformly (scalar-only included), which preserves any service-applied filtering and avoids an emitter-side predicate branch. `TypeFetcherGeneratordispatch assembles the service call and sets the loader value type toorg.jooq.Record. Validator:validateServiceTableFieldgains a return-table-PK guard (identity re-projection needs the returned table’s PK), mirroring the parent-PK check. Coverage: executionGraphQLQueryTest.films_castMembers_referenceSubfieldResolvesViaServiceTableFieldLift(mapped + list,Film.castMembersoverFilmActorwithactor: Actor @reference, asserting exact per-film cast so the reference resolves, no cross-parent leakage, no widening); pipeline both containers (FetcherPipelineTestpositionalloadFilmsreturnsList<List<Record>>+serviceField_mappedContainer_rowsMethodReturnsMapOfProjectedRecord); unitServiceFieldValidationTest.RETURN_TABLE_NO_PK; structural pins updated to the projected-Record loader value (TypeFetcherGeneratorTest,UnifiedEmissionPinsTestskeleton-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 sakilafilm_actorrows 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 `@keydirective on it isresolvable: false(no demote, noEntityResolution): 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_entitieshandler. When at least one key is resolvable the R176 table-required diagnostic still fires. The decision turns on the federationresolvableflag alone, never on@record(or any) classification; a reachable stub then rides the ordinary classified-type path into the served_service.sdlcarrying 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 at01696e1was reverted at77362d3(orphans never reach the runtime SDL, whichGraphitronSchemaClassGenerator.planForbuilds from the registry, so the relaxation only suppressed the error without surfacing the type). Coverage: unitEntityResolutionBuilderTest.resolvableFalseKeyOnRecordType_isAcceptedAsReferenceOnlyStub+mixedResolvableAndNonResolvableKeysOnRecordType_stillRejects; execution-tierFederationBuildSmokeTest.serviceSdlExposesNonTableBoundResolvableFalseStub(newFilmRefStubfixture, a non-table-bound service-bound record carrier reachable viaQuery.filmRefStubs, asserts the type and its@key(... resolvable: false)reach the served{ _service { sdl } });resultEntityUnionContainsAllFixtureEntitiesupdated to expectFilmRefStubin_Entity(federation-jvm includes every@keytype regardless of resolvability, exactly like the table-boundLanguagestub; benign sinceresolvable: falsegoverns composer routing, andFederationEntitiesDispatchTeststays green with the resolution-less member present). Follow-up R289 filed to correct theKeyNodeSynthesiseropt-out javadoc, which still claimsresolvable: falsekeeps a type out of_Entity. Full reactor green. -
R292 (
4102697+ self-review7a34e4e): Descriptions on synthesised Connection/Edge/PageInfo boilerplate. Graphitron-synthesised relay types carried no SDL descriptions, tripping Apollo’sALL_ELEMENTS_REQUIRE_DESCRIPTIONlinter on every generated Connection/Edge type and field (20+ violations on a real consumer schema). The fix lives entirely inConnectionPromoter, the single synthesis site: thirteen canonical graphql-relay-js wording constants with.description(...)added to the type builder and each field definition inbuildSynthesised{Connection,Edge,PageInfo}. Single source of truth, parity for free: the description rides on the synthesisedGraphQLObjectTypecarried byConnectionType/EdgeType/PageInfoType.schemaType(), so it lands on both published seams (SchemaSdlEmitter’s `SchemaPrinterfile output andObjectTypeGenerator’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: unitConnectionPromoterTest.directiveDrivenSynthesis_carriesRelayDescriptionsOnTypesAndFields(asserts descriptions onschemaType()and every field against the GraphQL model, including synthesised PageInfo whose fixture declares none); pipelineSchemaSdlEmissionTest.synthesisedConnectionBoilerplateCarriesRelayDescriptions(re-parses the emittedschema.graphqlsstructurally, confirming descriptions survive theSchemaPrinterseam); pipelineSynthesisedConnectionRuntimeDescriptionTest.runtimeRebuiltSchemaCarriesSynthesisedConnectionDescriptions(reads descriptions offGraphitron.buildSchema’s runtime `GraphQLObjectType, a genuine pin of the runtime seam thatFederationBuildSmokeTest.emittedSdlMatchesRuntimeSchema’s `SchemaDiffingcannot see because it does not walk descriptions as graph vertices). The sakila fixture declaresPageInfostructurally, so the pipeline tests scope to the genuinely-synthesisedQueryStoresConnection/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@asConnectionsame-table required-@nodeIdhygiene advisory now ridesctx.addWarning(new BuildWarning(..., locationOf(fieldDef)))instead of the dedicatedASCONNECTION_HYGIENE_LOGSLF4J category, which is retired, so every classifier advisory surfaces onschema.warnings()as one inspectable record;AsConnectionSameTableWarnFormatTestmigrated off logger capture ontoschema.warnings(). Phase 2 (cleanup + gate): removed the 11 redundant@recorddirectives and the redundant@splitQueryfrom the sakila-example schema (their warning paths stay owned on minimal SDL byR96RecordBindingPipelineTestandSingleRecordTableFieldServiceProducerPipelineTest), and addedFixtureWarningsGateTest, which builds the example schema viabuildOutput()and assertsschema.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:filmsConnectionByRequiredIdsexists 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-facingfailOnWarningMojo feature with typedWarningKindallowlist, deprecated-usage warnings (R296), and R293’s remaining non-generator warning categories. Split out of R293. Full reactor green. -
R287 (
6e3ef42): Remove the DELETE →@tablereturn path. DELETE cannot legitimately project a full@table: the row is gone after the statement andRETURNINGcarries 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.validateReturnTyperejects a direct-return DELETE →@table(closing theMutationDeleteTableField@tablepath so it only ever holds anEncoded*arm), andFieldBuilder.classifyDeletePayloadFieldrejects a@table-element data field on a DELETE payload carrier, both naming why and pointing at the ID return.MutationDeleteTableFieldgains a compact constructor rejecting aProjected*DmlReturnExpressionarm as a runtime backstop (theProjected*arms stay live for INSERT/UPDATE/UPSERT, whose rows survive the statement); the spec considered and declined narrowing the component type via a sub-sealedDmlReturnExpressionon blast-radius grounds. The now-dead carrier and its support chain are deleted:ChildField.SingleRecordTableFieldFromReturning,PkResolution(whoseNonPkNullablenull-fill arm was the wrong behaviour itself),PerFieldOutcome,BuildContext.classifyDeleteTableProjection/DeleteTableProjection/classifyElementFieldForDeleteProjection,FieldClassification.SingleRecordTableFromReturning(+CatalogBuilder/LSP arms),FetcherEmitter.buildSingleRecordTableFromReturningFetcherValue(+ dispatch), theTypeFetcherGeneratordispatch entry, and theGraphitronSchemaValidatorno-op arm.SingleRecordIdFieldFromReturning(encoded-PK-off-RETURNING, deletion-safe) stays. Coverage: validation-tierMutationDeleteTableFieldValidationTestpins both author-facing rejections SDL-driven (no hand-built illegal field); the corpus retires the DELETE@tableverdict, repointing DELETE roots to[Dml]/Columnencoded-ID returns and dropping the DELETE payload examples (covered under the nodeidfixture, carried inVariantCoverageTest’s `NO_CASE_REQUIREDforMutationDeletePayloadField/MutationBulkDeletePayloadField);LeafTupleAdapter’s refusal arm and `PkResolutionEmitterReachabilityTestretire with the type; the sakila example dropsdeleteFilmsTableCarrier/DeletedFilmsTablePayload/DeletedFilmInfoand the execution proof, keepingdeleteFilmsIdCarrier. User docs (code-generation-triggers.adoc), model/generator javadoc, and roadmap cross-references corrected so nothing presents DELETE →@tableas 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@recordremoval.@recordis now parsed-but-ignored: the backing Java class is reflection-derived from the producing field (an@servicereturn, or parameter for inputs; a@tableresolution; a@tableMethodreturn; 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 atTypeBuilder.emitDirectiveIgnoredWarnings). The reference pagerecord.adocis rewritten as a deprecation/ignored page;deprecations.adoc+ both directive indexes mark@recordignored (precedent:@index);directives.graphqlsreframes the@recorddescription as DEPRECATED/IGNORED and drops@recordfrom theargMapping-inert list (matchingcheckArgMappingInert, which no longer fires for it); explanation pages, how-to guides, and the rewrite-internal adocs shift from@record-bound/-declared/-parentto reflection-derivedclass-backed/record-backedterminology with@record(record:)stripped from every worked example in favour of the producing field; javadoc acrossmodel/,catalog/, and generator classes reworded comment-only (the lone code change addsrecordtoDeprecationsDocCoverageTest.WHOLE_DIRECTIVE_DEPRECATIONS). The generatedsupported-schema-shapes.adocwas regenerated from the updated leaf javadocs (also resyncing pre-existing drift:PlainObjectTyperemoved per R276, newer leaves added);leaf-coverage --verifyreports up to date. Runtime diagnostic strings (@record parent,a @record type) left untouched as out of scope. Full reactor green. -
R312 (
2524d8c): ThreadCompositeDecodeHelperRegistrythrough 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@conditionfields. Part A: own-and-drain a per-class decode registry at the two class-assembly points that host reference-field filter sites;TypeClassGeneratorowns one registry per<Type>class (threaded throughbuild$FieldsMethod→emitSelectionSwitchincluding theNestingFieldrecursion intoInlineTableFieldEmitter/InlineLookupTableFieldEmitter),TypeFetcherGeneratorone per<Type>Fetchersclass (threaded intoSplitRowsMethodEmitter.buildFor*andbuildQueryLookupRowsMethod). A newCompositeDecodeHelperRegistry.collectInto(TypeSpec.Builder, Consumer<…>)bracketing helper co-locates construct and drain so a lifted helper can never be silently dropped, withQueryConditionsGeneratorrefactored onto it so there is one drain implementation. Part B: guard the empty-join-path (standalone-lookup) shape uniformly withParentCorrelation.checkCarrierInvariant;InlineTableFieldEmitteremits a correlation-free conditions-only subquery (synthetic terminal alias + pre-switchDSL.noCondition()seed),InlineColumnReferenceFieldEmitterprojects the column directly off the parent alias, andSplitRowsMethodEmitterthrows a descriptive classifier-invariant error instead of an opaqueIndex -1. TheArgCallEmitter:372null-registry throw is kept as the backstop. Coverage:NodeIdReferenceFilterPipelineTest(inline + split lift asserting the liftedprivate static decodeBar*helper, condition-only real-FK regression guard, empty-join-path standalone) plus agraphitron-sakila-examplecompilation-tier fixture (FilmMixedNodeIdConditionFilteron inline and@splitQueryreference fields) as the cross-module forgotten-drain backstop. No newSTUBBED_VARIANTSentry; thePROJECTED_LEAVES"fully implemented" claim stays honest. Full reactor green under-Plocal-db. -
R313 (
adfaeff+ build-through43645d9): 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 casescalar LocalDate @scalarType(scalar: "graphql.scalars.ExtendedScalars.Date"), whose constant is namedDate; and the convention siblingGraphQLBigDecimal, whose constant is namedBigDecimal) was emitted asadditionalType(<constant>), registering graphql-java under the constant’s name, so everytypeRef(<sdlName>)bound to nothing and the generatedGraphitronSchema.build()threwtype <SdlName> not found in schemaat runtime (surfaced downstream as Sikt’sSakMerknaderShapeTestbuild failures). The fix routes the mismatch through the existingScalarResolution.Synthesisedarm rather than teaching the emitter a new branch:ScalarTypeResolver.resolveFromConstantFqngains an SDL-name-aware overload that forks oncheck.scalar().getName().equals(sdlName)(match →Resolvedas before; mismatch →Synthesised(javaType, sdlName, owner, field), borrowing the constant’s coercing), with the SDL name threaded in viaresolveFromDirectiveValue/resolveByConvention(built-ins never alias, soresolveBuiltInis untouched);TypeBuilder’s `@scalarTypeand convention arms widen frominstanceof Resolvedtoinstanceof Successful, mirroring the federation arm. No new model component, no new emitter branch, no.javaType()reader change. Coverage spans all three tiers: resolver-tierScalarTypeResolverTest(alias →Synthesised, match →Resolved, theGraphQLBigDecimalsibling, and the convention-loop widened toSuccessful); pipeline-tierGraphitronSchemaClassGeneratorTest(alias emits thescalar_LocalDate()synthesised helper, not the bare constant;Moneyno-regression keeps the plainadditionalType(...MONEY)form);GraphitronSchemaBuilderTest.DIRECTIVE_BEATS_CONVENTIONcorrected to theSynthesisedoutcome (latently broken before); and the load-bearing execution / build-through added on review (43645d9): agraphitron-sakila-examplefixturescalar LocalDate+Customer.createDateon the realcustomer.create_dateDATE column, withgraphql-java-extended-scalarsat compile scope, whoseGraphQLQueryTest.aliasingScalar_registeredUnderSdlNameAndResolvesEndToEndasserts the assembled schema registersLocalDate(andDatedoes 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-validateb0f4305/6930c25, In Reviewd347cca): Single edge-driven classification pass and immutable validation;TypeBuilder.buildTypesretired. R279 left the reachable surface traversed three times (aSchemaReachabilityname-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 ofrejectDanglingTypeReferences). R317 collapses all three into oneSchemaReachability.walkdriving a realGraphQLTypeVisitor(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 onlyregister, 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-freeTypeBuilder.lookAheadVerdict(forced by graphql-java 25’s enter-only traversal, where a field’s output target is a not-yet-visited child).buildTypes, thereachableOutputTypeshand-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-producerDomainReturnTypeagreement) now register aValidationErroron a singleGraphitronSchema.diagnosticschannel (viaBuildContext.addDiagnostic) the validator drains, instead of demoting a settled verdict toUnclassifiedType/UnclassifiedField; a verdict read after the walk equals the verdict classification produced, and theValidationErrorstream / which schemas pass or fail stay byte-identical. TheNodeIndexis one-to-many by table (a table may back several@nodetypes); implicit-encoder ambiguity moved to a use-site rejection with a disambiguation hint, correcting the oldfindFirst()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 testSingleWalkClassificationOrderTest(a deep target’s type-classify trace follows its discovering field’s, which an eager type pass fails) plusNodeIdPipelineTest(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, andAppliedDirectiveEmitterTest(FEDERATION_SDLgiven a@tablesoUserclassifies). 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-interfaceparticipant cross-table reference fields to the@referenceterminal table.FieldClassification.ParticipantCrossTablewas the one column-bearing permit still in theFallThrougharm oflspColumnDispatch(), so the three column-name LSP consumers dispatched on the enclosing participant@tablerather than the terminal table; a single-table-interface participant field reaching a column on another table via@referencedrew a false-positiveUnknown column … on table '<participant table>'squiggle on a schema that builds clean, plus wrong-table hover and completion. Fix is a single-arm relocation toResolve(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, andFieldCompletionsTest. Full reactor green under-Plocal-db. -
R330 (
8197af1+84102d6+06b47de+e9d80fc): Fix@condition(override: true)on a@nodeIdFK-target filter field passing the parent’s root table instead of the joined FK-target alias, a v9→v10 parity gap surfacing asincompatible typesat consumer compile (e.g. aSoknadsmangeltypehanded toiRegelverksamling(Regelverksamling, ...)). An FK-target@nodeIdfield’s developer@conditionmethod expects the FK-target tableXreached through a foreign-key join path, not the input’s own table, but the rewrite’s no-joinliftedSourceColumnsmodel never propagated the join into the@conditionmethod’sParamSource.Tableslot, so the emitter passed the literal"table"for every condition method. The model gap is lifted into a sealedWhereFiltersiblingFkTargetConditionFilter(alongsideConditionFilter/GeneratedConditionFilter) carrying the targetTableRef, the resolvedFkJoinjoinPath, the lifted FK-child source columns, andX’s key columns; `FieldBuilder.walkInputFieldConditionswraps both the single-columnColumnReferenceFieldand compositeCompositeColumnReferenceFieldarms in it whenever the join path is non-empty. Every WHERE-emitting site forks on the type through a sharedFkTargetConditionEmitter(declareAliases+emitTerm) so the FK-target arm is defined once and cannot drift across the five sites (QueryConditionsGeneratorshim,InlineTableFieldEmitter,InlineLookupTableFieldEmitter,SplitRowsMethodEmitter,TypeFetcherGenerator.buildQueryLookupRowsMethod); the plain arm stays byte-identical. The FK-target arm emits a correlatedDSL.exists(DSL.selectOne().from(X).where(<correlation>.and(method(X, args))))(approach B over restoring the legacy top-level join, keeping the(Table, env) -> Conditionshim contract and staying inside theJoinStepcardinality invariant); the correlation reusesJoinPathEmitter.emitCorrelationWhere, which ANDs every FK slot, so composite-key FK targets work for free with noRowN. Recursing inline/lookup/split sites runtime-prefix their SQL aliases onto the base alias’sgetName(); the two top-level method sites use static aliases. Both reported instances fixed (SoknadsmangeltypeFilterInput.regelverksamlingIdshim path in pass 1;EndringsloggV2FilterInput.brukerIdinline 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: pipelineNodeIdOverrideConditionFkTargetPipelineTest(single-column + composite FK-target carrier assertions, no code-string assertions on method bodies); sakila compile-tier guards (concreteAddress/Projectcondition parameters) and execution assertions for shim, shim+field-override, inline child,@splitQuerychild, multi-field shim, composite@table, and plain-input composite on both list and@asConnection. Full reactor green under-Plocal-db. -
R338 (
f40b056+ warnings-gate371a5eb): Split-query correlation now keys both cardinalities off the FK’s referenced columns instead of the parent PK. A list@splitQueryreference field whose@referenceFK targets a non-PK unique key on the parent silently returned zero rows for every parent:FieldBuilder.deriveSplitQuerySourcebuilt theparentInputVALUES table from the parent’s PK columns on the List (child-holds-FK) branch, whileSplitRowsMethodEmitter’s correlation predicate references the FK’s actual referenced columns (sourceSideColumns()); when those are not the PK, `parentInput.field(...)resolved tonulland the predicate degraded tocol = NULL, matching nothing with no error raised. The fix drops the!isListguard so both cardinalities key off the first hop’ssourceSideColumns()when the first hop is anFkJoin(BuildContext.resolveFkSlotsalready orients a child-holds-FK first hop so the slot’s source side is the parent’s referenced columns), keeping theprimaryKeyColumns()fallback only for the non-FK first-hop (ConditionJoin) shape, whereParentCorrelation.OnConditionJoincorrelates on parent PK. The read-side machinery already reads arbitrary FK source columns off the parent record (the Single branch andderiveFkRecordParentSourceprove this), so no emitter change was needed; the stale parent-PK-assumption javadoc was rewritten. Coverage: execution-tierGraphQLQueryTest.splitTableField_fkReferencesNonPkUniqueKey_returnsChildRowsover a newsplit_parent(PKparent_id, UNIQUEparent_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@defaultOrderdirective-leveldirection:on theprimaryKey:andindex:variants.OrderByResolver.resolveOrderEntriesalready threaded the resolveddefaultDirectioninto thefields:branch but hardcodedSortDirection.ASCon the two sibling branches, so@defaultOrder(primaryKey: true, direction: DESC)(and theindex:variant) silently sorted ASC, violating the directive’s published contract (direction: SortDirection = ASCdeclared directive-level with no per-source carve-out). This reverses R243’s "fork (b)" for@defaultOrderonly: theprimaryKey:branch now stampsdefaultDirectiononto each synthesised PK entry, andresolveIndexColumnstakes aSortDirectionparameter that the@defaultOrdercall site feedsdefaultDirectionwhile the@orderenum-value alias still passesASC(its direction comes from the runtime input object’sdirection:field, flipped in the*OrderByhelper at code-generation time, not the directive). The directive-absent implicit-PK fallback inresolveDefaultOrderSpecstays 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 yieldsuniformAsc == falseand paginates descending end to end automatically. Coverage: pipeline-tierGraphitronSchemaBuilderTestrewroteDEFAULT_ORDER_DIRECTION_DESCin place (no stale fixture left alongside) to assertuniformAsc() == false/direction() == DESC, and addedDEFAULT_ORDER_INDEX_DESC; execution-tierGraphQLQueryTest.filmsConnectionDesc_executesDescendingPrimaryKeyOrderover a newfilmsConnectionDescconnection assertsfilmIdorder 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 (
fa07632Phase 1+2 +2dd6fbbPhase 3 +e066346Phase 4 + docs6d3ac86): LSP Java-source surfacing for goto-definition, Javadoc hover,@externalFieldcompletion, andargMapping. Framed as expanding the catalog dataCatalogBuilder.buildalready exports to the LSP, not a new feature: theSourceLocation/descriptionslots 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. NewSourceWalker(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 throughRewriteContext.compileSourceRoots(populated fromMavenProject.getCompileSourceRoots()inAbstractRewriteMojo).CatalogBuilderis the single join site: it rebuilds the immutableTable/Column/ExternalReference/Methodrecords from theClasspathScannerstructure plus the walk index in one pass (jOOQ half refined to per-line + field Javadoc; service half gains aSourceLocationcomponent withUNKNOWN-defaulting back-compat factories), keying methods on(className, methodName, paramCount)and dropping overload-ambiguous keys toUNKNOWNrather than binding a wrong line.Definitions.computegains a service-half arm reusingLspVocabulary.behaviorAt/siblingStringAtfor@service/@externalField/@enum/@condition/@sourceRow/@tableMethod(@recordcarved out, mirroring completion/hover);Hoversrenders class/method Javadoc;ExternalFieldCompletionsnarrows the method list to single-parameterField-returning lifters;ArgMapping(pure string-content decomposition) +ArgMappingCompletions+argMappingdiagnostics cover thejavaParam: graphqlArggrammar (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@externalFieldTable-parameter check uses the catalog-derivable signature shape (the classifier-drivenParameter.source = ParamSource.Tableprojection is generator-side work the LSP catalog does not carry), andargMappingdot-paths validate/complete the head segment only (the snapshot carries no nested input-field projection for arbitrary input types). Coverage: LSP-tierDefinitionsTest(one case per binding directive +@recordcarve-out + unknown-name / overload-UNKNOWNfall-throughs), pipeline-tierCatalogBuilderSourceTest(column + service-half refinement and Javadoc lift from a synthetic source root;UNKNOWNfallback when roots absent), unit-tierSourceWalkerTest(overload-ambiguity, doc-comment retention, unparseable-file tolerance, mtime cache invalidation, no params/locals as fields), plusArgMappingTest/ArgMappingDiagnosticsTest/ArgMappingCompletionsTest/ExternalFieldCompletionsTest/HoversTest; no code-string assertions (no generated output, positions /Location`s are the asserted shape). `getting-started.adocdocuments the editor surface. Predecessor R18. Full reactor green under-Plocal-db. -
R343 (
72440a0a0): LSP column-name completion / hover / validation for@defaultOrder(fields: [{name: ...}]). Binds theFieldSort.namecoordinate toBehavior.CatalogColumnBindingin theLspVocabularycanonical overlay; previously the site fell through toArgNameCompletionsand 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 relocatingTableTarget/RecordTableTargetfrom theFallThrougharm ofFieldClassification.lspColumnDispatch()toResolve(tableName())(the element table), the same single-arm pattern R331 gaveParticipantCrossTable; the@reference-backed shape already resolved viaParticipantCrossTable. No new completion provider. Coverage: pipeline-tierLspColumnDispatchProjectionTest(plain list,@asConnection @splitQueryconnection, and@splitQueryshapes all classifyTableTargetand dispatchResolve(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), andDriftDetectionTest(FieldSort.nameresolves 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, Spec5e9ae41): 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). Newmodel/ColumnOverlapintroduces a minimal read-onlyColumnWriterview (targetColumns()in decode-record slot order,decode(),label()), aContributor, anOverlapColumn(shared()/allPlain()), andgroupByColumn; a pure structural fold over already-resolvedsqlNamevalues invoked once per site, not a model-carried fact (the@mutationvalidator 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) retiresanalyzeOverlap+SlotRef, adaptingWriterinto the view;TypeFetcherGenerator’s `insertColumnPlan(site 2) andsetColumnPlan(site 6) delegate togroupByColumn, retiringInsertCol/InsertColWriterand R342’s cloneSetCol/SetColWriterontoOverlapColumn/Contributor,emitSetAgreementPreamble(site 4) replaces its inlinebyColumnmap /int[]tuples, sites 2 and 4 route their value-read through the sharedappendAgreementValue/emitAgreementDecodeLocalseam (already serving sites 5 and 6), andemitKeySetAgreementPreamble(site 5) adopts theSetGroupWriterleaf view + typed records while keeping its bespoke cross-partition intersection;MutationInputResolver(site 3) readsshared() && allPlain()off the same fold, making validator-mirrors-classifier structural. Each emitter downcastsContributor.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 sharedrequireColumnAgreementpredicate, dispatch partitions and theRejectiontaxonomy are untouched; site 1’s optional value-read fold and the outer gather-and-pairwise loop stay out of scope as specified. New unit-tierColumnOverlapTestpins 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. Newdefinition/DeclarationDefinitionsdispatches on the enclosing type’sTypeBackingShapevia an exhaustive switch with nodefault(mirroringDefinitionsoverBehavior, so a future backing permit forces a goto-def decision at compile time), resolving every arm through the sealedDefinitionTargetand the LSP-ownedSourceWalker.IndexR349 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 ofDeclarationHoversintoparsing/SdlDeclaration(sealedTypeName/FieldName) so the hover and goto-def triggers cannot drift;DeclarationHovers.findContainingbecomes a thin adapter over it. Corrected the signed-off D1’s inverted record/POJO premise: resolution is by source-index key, andMemberSlot.name()is the bean property name a POJO method index is not keyed by, so the slot is widened to carry the arity-0accessorMethodName, populated at the oneCatalogBuilderprojection site (projectPojopassesmethod.name(),projectRecordpassesrc.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/resolvewidened to package-private for the sibling provider; chained intoGraphitronTextDocumentService.definitionvia a third.or(). Coverage: pipeline-tierDeclarationDefinitionsTest(one case per backing shape per axis: table-class / record / POJO / standalone-jOOQ type names, theSourceAbsent→ 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),MemberSlotcall sites updated acrossFieldCompletionsTest/HoversTest/DiagnosticsTest; no code-string assertions (resolvedLocation`s are the asserted shape). Builds on R349 and R90. Full reactor green under `-Plocal-db. -
R366 (
d71545d, In Review143a155, Specb6a6f93/88e61592): EmitloadManydispatch for list-cardinality polymorphic@splitQueryon record-backed parents.MultiTablePolymorphicEmitter.buildBatchedListFetcherunconditionally emittedreturn loader.load(key, env), but for anAccessorCall/MANY(orProducedRecordRead/MANY) parentSourceKey,GeneratorUtils.buildRecordParentKeyExtractiondeclares a loop-localList<…> keysrather than a single method-scopedkey, so the generated fetcher referenced an out-of-scope local and failed javac (cannot find symbol: variable key) on a field that passedgraphitron:validate. This is the polymorphic sibling of the already-fixed non-polymorphic wrapper split-query compile bug. Fix forks the load site onparentSourceKey.cardinality(), mirroring howTypeFetcherGenerator.buildRecordBasedDataFetcherbranchesloadvsloadMany:ONEkeepsloader.load(key, env);MANYemitsloader.loadMany(keys, Collections.nCopies(keys.size(), env))then concats the one-bucket-per-elementList<List<Record>>into the field’s flatList<Record>viaflatMapbefore 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 theSourceKeythe fetcher already holds) and no new floor-guarantee rejection:AccessorCall/ONEis unreachable on a list field (FieldBuilder.collectAccessorMatchesrejects a single-record accessor asCardinalityMismatch) and single-cardinality Pojo is already deferred-rejected, so the two live list-arm paths areColumnRead/ONE(table parent) andAccessorCall/MANY(record parent), both now compiling. Coverage: compilation-tier fixtureOccupantsBatchPayload(free-form@recordexposingList<AddressRecord> addresses(), childoccupants: [AddressOccupant!]! @field(name: "addresses")over theCustomer | Staffunion) +OccupantsBatchPayloadService+Query.occupantsBatch, whose generatedOccupantsBatchPayloadFetchers.occupantsemits theloadMany`+flatMap` dispatch and fails javac on any regression toload(key); the pipeline tier already classifies theAccessorCall/MANYshape. Sibling of R367 (single-cardinality guard); sharesMultiTablePolymorphicEmitterwith R363. Full reactor green under-Plocal-db(:graphitron2216 tests,:graphitron-sakila-example455 tests). -
R364 (
172016a, In Review9eff0e4): Fix the@service @splitQueryrows-method return type for enum and non-built-in scalar leaf fields. A non-root@servicechild field whose GraphQL type is an enum (or any scalar outside the five GraphQL spec built-ins) generated a doubly-nestedMap<KeyRecord, Map<KeyRecord, V>>rows method instead of the flatMap<KeyRecord, V>, so the generated code did not compile; siblingInt/Booleanfields were already flat. Root cause:ServiceRecordField.elementType()fell back to the service method’s wholeMap<K, V>whenRowsMethodShape.strictPerKeyTypereturnednull(which it does for any nameScalarTypeResolver.builtInJavaTypecan’t resolve), andouterRowsReturnTypethen wrapped that map once more. Per the spec’s recommended option (a), the fix addsRowsMethodShape.perKeyFromOuter; the structural inverse ofouterRowsReturnTypethat peels the per-keyVback out of a known outerMap<K, V>/List<V>across the(isMapped, isList)cross-product, returningnullfor an unpeelable shape ; and routeselementType()through it for the non-built-in scalar leaf (other null-perKeycases keep the legacy whole-type fallback); both call sites deriveisMappedfromsourced.container()(stored verbatim intoLoaderRegistrationatFieldBuilder.buildServiceLoaderRegistration) so emitter and validator cannot disagree. Spec step 2 closes the validator gap:ServiceDirectiveResolver.validateChildServiceReturnTypeno 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-tierRowsMethodShapeTest(forward/inverse round-trip across the full(isMapped, isList)cross-product plus three null-rejection cases), pipeline-tierFetcherPipelineTest(enum-leaf mapped field emits the flatMap<Row1<Integer>, String>with theIntsibling unchanged; wrong-container field rejected at classify time), andTestFilmServicefixtures; no code-string assertions on generated method bodies.graphitronandgraphitron-sakila-exampletiers green under-Plocal-db(execution tier 455 tests);graphitron-lspnot exercised at review (nativelibtree-sitterunavailable in the review sandbox, a known environment gap). -
R371 (
1327de0+ self-review259ff2d): 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 newparsing/DeclTargetsealed 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.locateto aLocation(goto),DeclarationHovers.overlayto a Javadoc string (hover) ; so they cannot point at different declarations and a newTypeBackingShapepermit 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:)armHovers.columnHoverstill runs its own switch and still diverges on the F1/F3 cases; retiring it rides on the candidate follow-up that liftsDeclTargetontoBuilt). 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 onlyNoBacking.*yields neither; the table / column arms keepDescriptions’s SQL-comment-wins precedence. Goto behaviour is unchanged: `locatereproduces the prior per-armDefinitions.resolvecalls exactly. Two spec-stated deviations, both justified:Descriptions.classJavadocwas 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), andlocate/overlayare public test seams so the parity property is assertable without a tree-sitter round-trip. Coverage: tree-sitter-free unit-tierDeclarationHoverOverlayParityTestasserts the resolver per backing, the overlay text per variant, and the overlay-presence ⟺ jump-presence drift guard perDeclTargetvariant (F4); the liveDeclarationHoversTeststays classification-only via the back-compat 3-arg entry. No code-string assertions on generated bodies. Reviewed independent-session;DeclarationHoverOverlayParityTestgreen (5/5) and thegraphitron-lspreactor (-am) fully test-compiled under-Plocal-db; the live tree-sitter LSP tier was not exercised because the nativelibtree-sitterruntime 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 liveWorkspace, landing R118 slices 3-6 as thin reads on the R361 seam. Five tools plus one resource registered the waystatusToolis:services/conditions/recordsoverWorkspace.catalog().externalReferences()joined withWorkspace.sourceIndex();schemaoverWorkspace.snapshot()joined same-cadence with@nodemetadata offcatalog().nodeMetadata()(exhaustive switches over theTypeClassification/TypeBackingShape/FieldClassification/LspSchemaSnapshotpermits, nodefault);diagnosticsoverWorkspace.validationReport()reporting snapshot availability/freshness alongside; and adirectivesMCP resource (the newresourcescapability) 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.Methodgains a typedreturnsConditionfact classified at the parse boundary inClasspathScannerfrom the un-erased return descriptor (exactLorg/jooq/Condition;compare, so a consumer’s own type namedConditionis not mis-tagged), andDirectiveShapegains an applicable-locationsfield projected atCatalogBuilder.buildSnapshotfromDirectiveDefinition.getDirectiveLocations(). Shared wire mechanics (lenient arg coercion, opaque base64 page cursors, thefqcn#method/aritystable-ID grammar slice 7 will walk, and the typedSourceJoinleft join ; sealedResolved/NotIndexed/Ambiguous, never a silent drop or hard failure) factored intoMcpWire; 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-tierClasspathScannerTest(the parse-boundary condition classification incl. the false-positive guard),CatalogBuilderSnapshotTest(directive-locations round-trip + back-compat empty default), and MCP-handler tier inGraphitronMcpServerTest(services/conditions/records structured shapes with method refs and resolved/not-indexed location arms, the@nodecatalog 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’sgetSourceLocation()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-plannedBuildContext.locationOfline-arithmetic heuristic overdescription.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 indirectives.graphqls;BuildContextalso has no raw SDL to scan. Fixed in the LSP, which holds the raw source and a tree-sitter parse:Diagnostics.signatureRange/descriptionNameRangeresolve the validatorSourceLocationto a tree-sitter point, and when it lands inside adescriptionnode re-anchor the diagnostic range to the enclosing definition’sname(orenum_valuefor 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 throughvalidatorDiagnosticis re-anchored with noBuildContext/GraphitronSchemaValidatorcall-site changes. AddsDESCRIPTIONtoGraphqlNodeKind. 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:ValidatorDiagnosticsTestone 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(ValidatorDiagnosticsTest17 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 bareDSL.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-partDSL.name(baseTableSqlName, col)(renders"base"."col"and preserves theField<Object>the.eq(String)/.in(String...)predicates need, where a table-instance reference would type asField<?>and fail to compile), with the base table’s SQL name threaded throughbuildInterfaceFieldsList/buildCrossTableJoinChain/buildDiscriminatorFilter. Defect 2 (TypeBuilder.extractCrossTableFields): a participant@referencefield 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@referenceis 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 existingdrainBuildDiagnostics(the R204/R279/R317 pattern) as anINVALID_SCHEMAauthor error with file:line and a detail-column candidate hint. The spec called for validator-side emission invalidateTableInterfaceType, 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@referencefield stays valid. Fixture:jti_subject+jti_app_account+jti_personjoined-inheritance tables ininit.sql(detail tables re-declare the discriminator via composite FK), corrected-shapeSubject/AppAccount/PersonSDL in the example schema, execution-tier regression tests (GraphQLQueryTest.allSubjects_returnsDiscriminatorPerRow+allSubjects_inlineFragmentDetail_joinsWithoutAmbiguousColumnwith anSQL_LOGqualified-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 correlatedEXISTSfor 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-objectfilter:fields, the motivating utdanningsregisteret bug:STATUS_SELVAKKREDITERENDEonLARESTEDbound againstORGANISASJON, so the generated*Conditions.javadid not compile) or never read at all (direct scalarARGUMENT_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-siteWhereFilter): theEXISTSis emitted inside the generated<Type>Conditions.<field>Conditionmethod, so every call site (QueryConditionsGenerator,InlineTableFieldEmitter) stays unchanged and the correlation ties back to whatever alias the caller passes. Model: a new sealedBodyParam.RemoteColumnPredicate(joinPath, inner)wraps an ordinaryColumnPredicate(columns bound to the terminal table) rather than bolting ajoinPathonto the four operator/value-arity arms, mirroring howFkTargetConditionFilterwraps aConditionFilter;name()/list()/nonNull()/extraction()delegate toinner. Emitter (TypeConditionsGenerator): the four local arms collapse into oneColumnPredicatearm viaemitColumnPredicateTerm(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, buildsDSL.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 wholeEXISTS. Classifier (FieldBuilder): Surface 1 stops dropping the parsed path and wraps viaremoteIfReferenceJoin, discriminating plain@reference(Directextraction, terminal column → wrap) from the@nodeIdFK-target lift (NodeIdDecodeKeysextraction, local FK-child columns → stay local); Surface 2 reads@referencebefore the localfindColumn, parses the path, resolves the column against the terminal table, and carries it onScalarArg.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;@splitQueryrides the same path with no new work. Coverage: pipeline-tierReferenceFilterRemoteColumnPipelineTest(both surfaces lower toRemoteColumnPredicate; single-hop{table:}/{key:}, multi-hop, listIn; nodeId-stays-local discrimination guard;ConditionJoinrejection ; model-level assertions) and execution-tierGraphQLQueryTest(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.adocrewritten to state the correlated-EXISTSbehavior, 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-tierTypeConditionsGeneratorTestcases pin theEXISTSbody withcode().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 ofTypeConditionsGeneratorTestoff code-string assertions filed as a follow-up Backlog item. Builds on R379; siblings R236 / R282 / R330 and the deferrednodeid-fk-target-arg-join-translation. -
R391 (
c59a11c, Spec4cfb5dc): Add a default-caseGraphitron.newGraphQL()factory to the generated facade.GraphitronFacadeGeneratoremitspublic static GraphQL.Builder newGraphQL()with bodyGraphQL.newGraphQL(buildSchema(customizer -> {})), so a zero-extra-wiring consumer writesGraphitron.newGraphQL().build()instead ofGraphQL.newGraphQL(Graphitron.buildSchema(b -> {})).build(). Returns a builder (not a built engine), mirroring thenewExecutionInput(...)convention so instrumentation / execution-strategy configuration stays open without a second overload; the body delegates to the facade’s own single-argbuildSchema, keepingbuildSchemathe single schema producer, and in a federation-linked build that path already returns theFederation.transform-wrapped schema, so no federation-specific overload is needed. Call-site sweep: theGraphqlEngineconsumer exemplar plus 23 default-case execution-tier sites converted toGraphitron.newGraphQL().build()(enumerated in the landing commit); the two-argbuildSchema(b -> {}, fed -> {})federation sites, thefetchEntitiescustomiser, and the raw-SDL spike were deliberately left as-is. Coverage: unit-tierGraphitronFacadeGeneratorTest(structural only, no body-string assertion: method-list now expectsnewGraphQL, plusnewGraphQL_isPublicStaticReturningGraphQLBuilderandnewGraphQL_isPresentExactlyOnceInFederationBuild) and execution-tierFederationBuildSmokeTest.newGraphQLBuildsFederationWrappedEngine(builds vianewGraphQL().build(), asserts_service { sdl }resolves with no errors, the federation-wrap correctness the unit tier cannot reach). Example README updated to showGraphitron.newGraphQL(). Independent-session In Review → Done review; full reactor green under-Plocal-db. -
R99 (
e6df34d): Widen thegraphitron:devLSP 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, sogetAllProjects()is a single project and the@service/@condition/@recordclasses 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’starget/classesinto the scan side and itssrc/main/javaplus disk-discoveredtarget/generated-sources/*into the walk side, through one sharedsiblingModuleBasedirs()helper consumed by bothresolveClasspathRoots()andresolveCompileSourceRoots()(the codegen reflection loader widens for free through the former). Both halves ride the samecollectExistingDirsexistence 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 (noFiles.list, preserving catalog determinism) and noMavenProjectinstances are built for modules the session never loaded; a genuine standalone module finds no ancestor and is unchanged from pre-R99.DevMojoself-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 inexternalReferences()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 ingetting-started.adocand a CLAUDE.md note beside the catalog-jar footgun. Independent-session In Review → Done review; full reactor green under-Plocal-db(AbstractRewriteMojoTest13 tests). Builds on R351/R369. -
R256 (
2d13f72): Absorb the service walker substrate onto typed rejections + multi-arg ctors. R238 shippedServiceMethodCallWalkeras a behavior-preserving translator over an already-resolvedMethodRef.Service, so most of theServiceMethodCallErrortaxonomy it designed was unreachable: every reflection-time failure was produced upstream inServiceCatalogasRejection.structural(...)prose that lost its identity at the LSP boundary (Diagnostics.lspCodeOfreturnsnullforStructural). This item migrates those failures onto typed arms that flow through the existingWalkerResult/Diagnosticsubstrate, 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 newReflectionErrorsub-seal ofAuthorError(graphitron.reflect.*) carries the reflection-intrinsic failures shared across the three reflect helpers (reflectServiceMethod/reflectTableMethod/reflectExternalField) —ClassNotLoaded,ReturnTypeMismatch(with aReturnContextSERVICE|TABLE_METHOD discriminant selecting prose),ParameterNamesMissing,AmbiguousMethod— produced at the sharedpickMethod/ class-load / return-type sites so a@tableMethodfailure 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 underServiceMethodCallError(graphitron.service-method-call.*). Deliverable 3:checkServiceInstanceHolderShaperelaxed toresolveInstanceHolder— 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.InstanceWithDslHoldercarries the orderedctorParams, the walker translates them intoInstance.ctorArgsand raisesMultipleDslContextSlots(CTOR)for a multi-DSL ctor;methods.get(0)replaced bypickMethodso an overload tie producesAmbiguousMethodinstead of silently binding the first declaration-order match. Deliverable 4a:ConflictSite.sitewidened from a bareMethodRefto a sealedSite(Method|Carrier), retiring theContextArgumentClassifier.syntheticServiceMethodRefsentinel that fabricated an emptyMethodRef.Servicejust to satisfy the old field;ResolvedContextArg.sitescarries the widenedSite. Per the Spec’s split-it-out clause, the wire-coercion cast guard stays R261’s (R256 leavesscalarLeafemitting as-is and only guarantees the typed-rejection channel), and deliverable 4b (bean-helper-queueValueShape→ syntheticCallSiteExtraction.InputBeanround-trip) is carved out to R402 (retire-bean-helper-queue-valueshape-roundtrip). Coverage: unit-tierServiceMethodCallWalkerTest(ctor-source translation in order, CTOR-round multi-DSL error),MethodRefCallShapeTest(multi-arg holder ctor + context-only-needs-no-dsl emit),ServiceCatalogTest; pipeline-tierServiceRootFetcherPipelineTest(ReturnTypeMismatch SERVICE+TABLE_METHOD, AmbiguousMethod, multi-arg-ctor resolves without holder rejection) andServiceFieldValidationTest(InstanceHolderUnconstructible) assert on typed arms and stablelspCode`s, no code-string assertions on generated bodies; drift guards `RejectionSeverityCoverageTest(a sample per new permit) andSealedHierarchyDocCoverageTest(typed-rejection.adocparagraph + 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 R389JoinedTableInheritancePipelineTestfailures predate this work on an untouched classification path). Builds on R238; pins the boundary with R261; spawns R402. -
R450 (
cf2c34c+ rework1c0126d, Spec7f7c35d/a92bbcb): Fix the split-path hop-0 condition filter binding the same alias as source and target.SplitRowsMethodEmitter.buildWhereConditionemitted a hop-0condition:filter asmethod(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.OnConditionJoingeneralizes to the parent-anchor armOnParentJoincarrying only(firstHop, parentTable)with nocondition()accessor (consumers dispatch the hop-0 attach onfirstHop.on():ColumnPairs→ forward join,Predicate→ two-arg condition call);BuildContext.buildParentCorrelation(single producer) lands any hop-0filter()on that arm regardless of itsOn, keepingOnFkSlotsonly for filter-less FK/lifted heads; the batch grain becomes a projection off the arm (parentKeyColumns(): FK source columns / parent PK / routine inputs) read byderiveSplitQuerySource, so parent-PK grain iff parent-anchor topology is structural; the split emitter anchorsparentAliasand 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 viaAuthorError.Structuralnaming the escape hatch (previously classified unverified ; Check 2 skips a null originTable). Same-commit consumer audit: three inline emitters re-dispatch onfirstHop.on()(behaviour-identical), split-rows siblings share the parent-anchor path,TypeFetcherGeneratorholds noParentCorrelationswitch. Coverage: pipeline (hop-0 filter → parent-PKsourceKey+OnParentJoin; hop-1 sibling → slot key +OnFkSlotsunchanged; inline hop-0 filter →OnParentJoin), record-parent hop-0-filter Structural rejection fixture,OnParentJoinunit invariants +parentKeyColumnsprojection, and the execution-tier grain proof (twosplit_filter_parentrows sharingtarget_id=1with oppositeincludevalues; split reproduces inline per-parent rows; the concretely-typed condition method also made the pre-fix double-bind fail compile-spec). Rework pass1c0126drepaired the three stale terminal-back-walk javadocs inSplitRowsMethodEmitter(the R449-absorbed housekeeping). Independent-session In Review → Ready → In Review → Done review; full reactor green under-Plocal-dbon both passes. -
R451 (
3ce199b, Spec7ecdf89): Routine writes ;@routineon a Mutation field commits before the follow-up query. A Mutation field carrying@routine(aVOLATILEtable-valued function) plus at least one@referencehop classifies as the new sealed leafMutationField.MutationRoutineWriteField(verbOperation.RoutineWrite) and emits the DML two-step transposed onto the R435 chain: step 1 executes the routine insidedsl.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 sharedRoutineChainrecord (one invariant enforcer spanning read and write leaves, exposed via theRoutineChainFieldcapability interface,ServiceFieldprecedent);buildKeysInConditiongeneralizes the DMLbuildPkKeysConditionso both two-step fetchers share the composite-safe key-IN condition.JooqCataloggainedRoutineResolution.NonTableValuedRoutine(a verified probe of the generatedroutinessub-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@routineand a condition-joined or filtered hop 0 (no derivable post-commit re-read anchor) likewise land typedDeferred`s. Root-head and multi-routine rules extend to Mutation chains; sakila gained `public.rent_filmand the scalarpublic.rental_count_for_customer(schema 2.8 → 2.9). Coverage:ClassifiedCorpusroutine-mutation-writeentry,GraphitronSchemaBuilderTestR451 block,RoutineMutationWritePipelineTesttwo-step fingerprint pin (sanctioned call-site form), and execution-tierrentFilm_*(commit observed by independent read; failing routine rolls back with nothing committed).@routinedirective 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+ rework7b86287/09ebe0d/171d468/ce5149d/11cf1e3): Graphitron owns the connection lifecycle ; application runtime, operation-typed transactions, and database-mounted session identity. An emitted application-scopedGraphitronRuntimeowns the consumer’sDataSource; every operation pins exactly one connection (safe because batch loaders run SQL synchronously on the dispatch thread, tripwired atRowsMethodCallTest); the caller’s claims travel as an opaqueStringto 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 emittedGraphitronTransactionProvider(commit-policy axis:ROLLBACK_ONLYis R428’s rollback-everything dev mode);@deferstays 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@servicepresent; Oracle/RAS execution coverage is R468). Per-request entry isGraphitron.newOwnedExecutionInput(claims, ...)beside the R190 escape hatch (kept, with a one-time caller-owns-everything wiring notice); the tenant-keyedTenantConnectionscarrier lands the acquisition seam R45 consumes. Docs:runtime-extension-points.adocrewritten 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-dbon both passes. Builds on R190; feeds R45/R428; spawns R460, R468, R469. -
R428 (
5488bc4/ae91f86/29420c8/ab56510+ rework43de546/3758ec3, Specfd33e86): MCPexecutetool runs a GraphQL query/mutation against the generated resolvers in-process in thegraphitron:devJVM, 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 emitsGraphitronDevExecutorinto the output package, compiled in the same R410 pass, exposing onepublic 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 (thenewOwnedExecutionInputsignature, 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-connectionDataSource, constructs the R429GraphitronRuntimewith the requested dialect, and runs under theROLLBACK_ONLYcommit policy so the dev loop exercises the same acquisition/hook/transaction path a real app does while never persisting a write. Host half (DevQueryExecutorin graphitron-mcp): a fresh platform-parentedURLClassLoaderper call overtarget/graphitron-classesfirst (R410 shadowing invariant) plus the consumer classpath, JDBC driver discovered viaServiceLoaderon 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 namingGRAPHITRON_DEV_CLAIMS; per-call claims override rejected unless opted in. R429 contract change flagged and reviewed: theROLLBACK_ONLYarms 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.releasediscards everything) because the shipped R449/R451 DML two-step reads back committed state; the one stated fidelity limit (no mid-operationafterSettlere-fire under this policy) is documented in the provider/executor javadocs and pinned by a provider unit test. Federation_entitiesexecution 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-tierDevExecuteExecutionTest(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 missingmojo-configuration.adocrow 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 resolvingResolutionScope.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 CLImvn graphitron:dev) is inherited by the federated/multischema generate executions via Maven config merge, so those fixtures now additionally emit aGraphitronSessionHook; harmless (build green) but a behavior change the landing commit described as "unaffected". Builds on R410/R429/R118. -
R308 (
1481592/0803628+ reworkcbe34fb/324e69c/6791638, Spec7517f93): Carrier arrival modeled on the@servicepayload 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 atFieldBuilder.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@servicefields may return the same payload with different arrivals), andCoherentcarries the required producer cardinality socheckServiceReturnMatchesPayloadconsumes 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).ServiceCarrierShapeErroris a new sealed sub-seal ofRejection.AuthorErrorwith typed arms carrying the disagreeingSourceKey.Cardinalityaxes and stablegraphitron.service-carrier-shape.*LSP codes:ProducerArrivalMismatch(list carrier, single-value producer ; subsumes the a1 silent runtime-crash admit and both misleading record-handoff rejections) andDataFieldArrivalConflict(list carrier whose@table-element or class-backedRecordElementdata field is itself a list over a flat producer collection ; both per-requestClassCastException`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@tablesingle-data-field list carrier (pipeline + sakila execution fixtureserviceFilmsByIdsAsPayloadsproving 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/ClassifiedDslTestpins) 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-dbon every pass, unpiped exit codes. Builds on R305/R316/R329; feeds R463. -
R477 (
cb42d90, Spec15c679f): Batch entity dispatch rejects well-formed node ids whose decoded key has the wrong arity for a composite-key@nodetype, instead of crashing (under-arity) or silently resolving the wrong row (over-arity). The batch path (EntityFetcherDispatch, drivingQuery.node/Query.nodesand federation_entities) decoded aKeyShape.NODE_IDid inHandleMethodBody.emitDecodeAndGroupwith a null-only guard and sizedcolsby the runtimedecoded.length, while the pairedSelectMethodBodyindexescols[i]for every column inalt.columns()(the fixed composite-key count): an under-arity id (e.g.Opptak:999against a 2-column key) trippedArrayIndexOutOfBoundsExceptioninsideselect<Type>Alt<N>; surfacing as a redacted 500 throughnode/nodesand as the raw, unredactedIndex 1 out of bounds for length 1machine-to-machine through_entities; and an over-arity id silently resolved the row keyed by its valid prefix. Fix tightens the emitted guard toif (decoded == null || decoded.length != alt.columns().size()) continueand sizescolsfrom 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 guardvalues.length != N(NodeIdEncoderClassGenerator,InputBeanInstantiationEmitter).DIRECTalternatives are untouched (they size byrequiredFields, contract-equal tocolumnsfor that shape). Fixture: the federatedFilmActorgained@node+@nodeId(its_Entityunion membership already came from@key, so the smoke test’s set is unchanged) to give the federation path a compositeNODE_IDalternative. Coverage: execution-tier onFilmActor(composite PK actor_id/film_id) across all three entry points ;GraphQLQueryTest.node_compositeKeyWrongArityId_returnsNullandnodes_compositeKeyWrongArityIds_nullSlotsPreservePositions(under- and over-arity slots null, valid slot resolves, positions preserved),FederationEntitiesDispatchTest.entities_compositeNodeIdUnderArity_yieldsNullSlotNoErrorandentities_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: sealingKeyAlternative.KeyShapeintoDirect/NodeIdvariants (the prose-only invariant split that gave this bug a place to live). -
R201 (
b6ad9e5, Spec2f946d1/1a9cb30/867a53a): Honor@field(name:)in@errorpayload construction shape resolution, the output-side mirror of R200 and the last leg of the@fieldsymmetry set (R191 output reads, R202@errorextra-field reads, R200 input binding all Done). Construction of a class-backed error-channel payload previously read no@field: the mutable-bean arm matchedset<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:resolveErrorChannelfolds each SDL field into a FieldBuilder-internalPayloadSdlField(sdlName, javaBaseName, fieldDirectivePresent)carrier (house idiomargString(f, DIR_FIELD, ARG_NAME).orElse(f.getName())), where the third component is load-bearing and cannot be reconstructed from the first two (javaBaseName == sdlNameis 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 onhasAppliedDirective). Architect-reviewed against promoting the carrier to a model type: the sole consumer is this one classify-time site and only the resolved artifact (setterMethod/ 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: oneresolvedErrorsCtorIndex(name-matched under the directive when parameter names are available, record components always or POJO with-parameters;errorsFieldIndexotherwise) feeds BOTHcollectDefaultedSlotsandErrorsSlot.CtorParameterIndexfrom 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. DeadsdlFieldNames(String)helper removed. Coverage: unitPayloadConstructionShapeTest(remap admit, remapped-but-missing reject naming SDL field + directive value + parenthetical, camelCase of the directive value), pipelineErrorChannelClassificationTest(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-freecodereferences.noparamsfixture package whose@fieldvalue 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.adocaxis sentence +result-types.adocpayload-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, leavingresolveErrorChannelthe single live call site. -
R502 (
70dd329, Specdd0b800): The fact "this type’s runtime carrier is a generic jOOQRecord`" 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 aninstanceofdisjunction 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 tocase JooqRecordCarrier _),FieldBuilder.resolveRecordAccessor(complement guard reads the direct fact); javadoc groupings inFieldBuilder.resolveRecordAccessorandChildField.PropertyField/RecordFieldrepointed to{@link}the new symbol. The read strategy (typedTables.X.COLconstant vsDSL.field(name)) stays deliberately per-leaf; identity forks (GeneratorUtils.recordColumnReadArgs,resolvePolymorphicRecordParent,CatalogBuilderprojections,TypeBuildermint,EntityResolutionBuilder.kindLabel) untouched. Pure refactor, byte-identical emitted output; acceptance is the existing tiers plus compiler-checked exhaustiveness of everyResultTypeswitch against the restructured permits. Independent-session In Review → Done review (census re-run clean;SealedHierarchyDocCoverageTestconfirmedRejection-scoped, no mapping needed); full reactor green under-Plocal-db. -
R495 (
d159757, Spec9d1a788): The per-class javadocInputRecordGenerator.buildClassSpecstamps 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>.inputspackage 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, Spec9d1a788): The hand-maintained caller census inFederationSpec’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 Reviewd2f4a61, Spec83891f2): Replace the two-nullable-fieldBuildContext.ConditionResolutionrecord, whose javadoc falsely asserted "exactly one ofrefanderroris non-null" whileresolveConditionRefreturned(null, null)in two places, with a sealed tri-stateResolved(MethodRef)/Failed(String)/Unresolved(). The record was the outlier among its own neighbours (FkJoinResolution,ConditionJoinTargetResolution,ArgBindingMap.Result/ParsedArgMappingare all already sealed switches), so sealing it removed ceremony rather than adding it: the tri-state contract is now compiler-enforced by an exhaustiveswitchat each of the threeparsePathElementcallers, replacing an orderederror() != null/ref() == nullnull-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, threeFailed, oneResolved) and the false "exactly one non-null" javadoc is gone with no roadmap-id citation in its place. Behaviour-preserving refactor: identical messages reach theerrorsaccumulator 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 typedRejection(Failed(String)matches the localerrors.addidiom), neighbouring sealed results untouched. Independent-session In Review → Done review;graphitronmodule and all named tests green under-Plocal-db(the sole reactor failure a pre-existingBgeEmbedderOnnxTestfloat-nondeterminism flake ingraphitron-mcp, a module the diff never touches, passing on rerun). Post-R438 residue. -
R503 (
46f5d95/f55ec5a, In Review03c2fc3, Spec3bbc3d8/5d525d4): A directiveless output type reached both as a nesting projection of a@tableparent (source is a generic jOOQRecord) 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 onsource instanceof org.jooq.Record, replacing the former deterministic validate-time rejection (the dissolvedConstructorFieldshape). 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-FieldCoordinatesfact onGraphitronSchema.reachableSourceShapes, read by the dispatch emitter and the validator’s shape-set rule so neither re-derives it;ReachableSourceShapecarries theSINGLE_ARM/DISPATCHED/REJECTEDpartition. Classification:TypeBuilder.isNestingEdgeTargetadmits aResultTypenesting target whileNestingTyperegistration stays gated on the strictisDirectivelessNestingTargetpredicate, so the type-level winner is walk-order-independent (the nesting facts live edge-side on theNestingField`s). `FieldBuilderde-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 / duplicateTypeSpec) andFetcherEmitter.bindDualShapecomposes 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, thecollectDomainReturnTypeConflictsDomainReturnType-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 toUnclassifiedType/JooqRecordCarrierand escapes suppression, still rejecting); theJooqRecordCarrier+ 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@classifieddimensions don’t express the cross-edge shape-set union); the behavior is pinned instead byMixedSourceNestedTypeReadsTest. 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), theReachableSourceShapeCoverageTestshape-set partition meta-test, compilation tier (FilmBlurbsakila-example fixture under<release>17>), and execution tier (MixedSourceNestedTypeExecutionTestdrives 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).ConstructorFieldValidationTestretired, 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-existingBgeEmbedderOnnxTestnative-binding flake ingraphitron-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 Reviewecb6e38, Spec27b7e9f):@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 holdingmax(<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’sOversatteTeksterServiceshape). Model: three newChildFieldleaves 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-inputVALUEStable andGROUP BY __idx__, scattered single-per-key), andPivotSlotField(one fact: the derived read name) ; composing a sharedPivotSpec(single-FK-hop path pinned by compact constructor, on/valueColumnRef`s, projection type, slot → token map resolved at classify time from the `vocabulary:enum’s@field(name:)values or by identity), plus anOperation.Pivotverb and aFieldClassification.PivotLSP projection. No new type variant: the projection type registers as the ordinaryNestingTypebehind 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@serviceresult. 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 ofRejection.AuthorErrorwith stablegraphitron.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: thepivotcorpus 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), andPivotExecutionTestagainst 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.adocPivotError section. Out of scope, named for later items: aggregates other thanmax, 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 Reviewa81e30e, Spec75eeaed/2400879): Scrubbed the deadChildFieldleaf taxonomy (SingleRecordTableField,RecordTableField,SplitTableField,RecordLookupTableField,SplitLookupTableField,LifterLeafKeyed,RecordTableMethodField) from present-tense comments, javadoc, and test-method names acrossgraphitronmain 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-sourcedBatchedTableField/BatchedLookupTableFieldarms,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 retiredPojoResultType.NoBackingpromotion claim replaced by the actual RETURNING-tableJooqTableRecordTypebinding,Wrap.TableRecord/Reader.ResultRowWalkkey mechanics repointed toKeyLift.ProducedRecords, and the@lookupKeydirective 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, andSingleAccessorOnListFieldwas confirmed live (a declared test dummy), not dead vocabulary. Independent-session In Review → Done review; full reactor green under-Plocal-db. -
R45 (
e6b955a..5364cbfslices 1-6, reworkf8027fd; Spec1613680, first In Review5364cbf, rework requesteda23ab82, second In Review0468f03): 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 sealedTenantBindingarm computed once post-walk from the column mappings the schema already carries (ArgumentBoundwith co-binding set and runtime agreement guard, per-rowNodeIdBound/EntityRepBound,Inheritedvia graphql-java localContext hand-down,Untenanted), so the generated fetchers acquire the right per-tenant DataSource through the R429TenantConnectionscarrier with no@tenantIddirective and no request-scope tenant parameter. Every fan-out point partitions per tenant: node/_entitiesdispatch groups per (alternative, decoded tenant) through the sharedresolveByRepsseam, DataLoader identity joins an opaque tenant segment through a single generated naming seam, and in multi-tenant buildsConnectionResultcarries the fetcher’s routedDSLContextso the lazytotalCount/facetsaggregate 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 silentUntenantedfallback; 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), themultitenant.graphqlsL5 fixture, andTenantDivinedRoutingExecutionTestagainst real database-per-tenant PostgreSQL (isolation, inherited batch routing, unknown-tenant pre-SQL error, mutation routing, cross-tenant nodes batch partitioning). Accepted coverage shape:_entitieswire-level execution rides the nodes proof through the shared dispatch surface, with the@keyfield-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-dbon 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, rework1f8340e; Spec220db01, first In Review5e14f49, rework requested9576465, second In Review77d9e9c): Normalized the DML reentry correlation onto the VALUES-join primitive: therows<Name>companion (the projected / discriminated mutation’s follow-up SELECT) resolves its correlation through one seam at two cardinalities ; the bulk arm renders the sharedVALUES (idx, key...)derived table (ValuesJoinRowBuildercore) joined over the carried correlation and ordered byidx, the single arm the legible degenerate, plain key equality, byte-identical to the retired keys-IN spelling. The classifier attaches the PK-self-identityParentCorrelation.OnLiftedSlotsfact to the fourDmlReturnExpressionreentry arms (constructed once inFieldBuilder.buildDmlReturnExpression; emit sites read the carried fact, never re-derive the key set), rejects a@tableDML return on a PK-less table at classify time (model-tier pinned overfilm_listat both cardinalities after the rework pass), andvalidateDmlReentryKeyAritymirrors 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 thecreateKeyedNodesinput-vs-key-order fixture, the missed-row cardinality shrink, a directrowsUpdateFilmsduplicate-keys companion-seam call, and discriminated bulk order +__typenamerouting overcreateContents.buildPkKeysConditiondeleted;buildKeysInConditionnarrowed 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-dbon both passes. -
R508 (
e7e5e56/7c9bb82/43a476f/5a84b88slices 1-4, review fix63aadb8; Spec6db96fc, review pass238b6a7, Ready64e6a2d, In Review80d1722): Dissolved the composite-column leaf family on all three classification axes: theCompositeColumnField/CompositeColumnReferenceFieldpairs onChildFieldandInputFieldand theCompositeColumnArg/CompositeColumnReferenceArgpair onScalarArgmerged into their arity-1 siblings under fresh names (ColumnBackedField/ColumnBackedReferenceFieldper field axis,ColumnBackedArg/ColumnBackedReferenceArg), so arity is acolumnscount (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 impliesNodeIdEncodeKeys/NodeIdDecodeKeys, documented as a deferred-generalization seam with theDirect-implies-arity-1 corollary;ParentCorrelation.checkCarrierInvariantunconditional on the output reference arm), and arity is classified once on the derivedisComposite()accessor that every consumer reads. The rooted-at-parent NodeId reference deferral moved enforcer in the same commit as the stub removal:GraphitronSchemaValidator.validateColumnBackedReferenceFieldrejectsNodeIdEncodeKeyscompaction asDeferredat every arity (the arity-N half is a new validate-time rejection replacing the generator stub), keyed by the merged class throughRejection.StubKey.VariantClass, leavingTypeFetcherGenerator.STUBBED_VARIANTSempty; the merged non-reference output leaf landed inIMPLEMENTED_LEAVES, the reference leaf inPROJECTED_LEAVES, withGeneratorCoverageTest’s partition exhaustive-and-disjoint throughout. The INSERT carve-out became arity-gated with the rejection re-worded to arity phrasing; the 22-slot `RecordNcap survived as conditional validator checks;FieldClassification.CompositeColumn*projection variants kept wire-stable, re-derived fromisComposite()inCatalogBuilder. Coverage:ColumnBackedFieldInvariantTest/InputColumnBackedFieldInvariantTest/ColumnBackedArgInvariantTestpin the constructor invariants per axis;NodeIdPipelineTestgained validate-tier assertions that both arities of the rooted-at-parent reference reject asDeferredon the mergedStubKeyanchor. Generated sakila output byte-identical through the slices; the review fix then dropped the composite-aware arm-ordering pin inTypeClassGenerator(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; Specb7823c9, Ready023e2bf, In Reviewb2d3d07): Documentation staleness prevention landed as one escalating pipeline (declare, sweep, promote). The "Documentation names only live tests/code" corollary indevelopment-principles.adocrewritten 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.adocprecedent) as first choice for any enumerable doc surface.roadmap/workflow.adocgained the## Retired vocabularyitem-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 inCLAUDE.md.RetiredVocabularyGuardTestpromotes 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, authoreddocs/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 toJavaSourceRegions(new code projection pinned byJavaSourceRegionsTest; comment/string projections still pinned throughRoadmapReferenceScannerTest), walk scope shared viaGuardScopeso the two prose guards cannot drift apart. Independent-session In Review → Done review; full reactor green under-Plocal-db. -
R513 (
259cdff, In Review6d9ea8c; Speccee5724, Ready5486708, In Progress04d21fd): 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.EventRecordvsmultischema_b.…records.EventRecord), and a*Fetchersclass binding both (cross-schema@servicemutations) emitted twocreateEventRecordhelpers with the same signature and failed to compile, because every helper-name site keyed the stem onClassName.simpleName()and deduped only within a full-ClassNamegroup. IntroducedFetchersHelperNames, one resolver per*Fetchersclass and the single home for thecreate*(jOOQ record + bean) anddecode*method-name namespaces: thecreate*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 keepssimpleName()(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 thecreate<A>List-vs-bean-AListoverlap).JooqRecordHelperNameskeeps its shape-contention machinery but now draws each class’s base stem from the cross-classcreate*map, so cross-class disambiguation and within-class shape ordinals compose (createMultischemaAEventRecord1/2). The three uncoordinated static"create"/"decode"+simpleName()derivations inServiceMethodCallEmitterandInputBeanInstantiationEmitterare 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*FetchersTypeSpec 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 newrewrite-generate-multischema-mutationexample slice binding bothEventRecord`s through service methods of distinct record types so the `-release 17compile proves both compilability and routing.CompositeDecodeHelperRegistryand 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 Review07c95f8; Spec1ad4b3e, Ready0b32af9): A@reference(path: [{key: "<fk>"}])foreign-key name resolved throughJooqCatalog.findForeignKeyacross 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 (sourcenull, non-table-backed, or itself ambiguous), the lookup returnedAmbiguousand the author had no lever to disambiguate: unlike@table(name:)’s `schema.tableform, thekey: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.noteandmultischema_b.noteboth holdnote_event_fk). Akey:value may now carry a leadingschema.qualifier (multischema_a.note_event_fk) scoping the FK candidate set to the FK-holder (child / referencing) schema, symmetric to the@tableprecedent, with unqualified behaviour byte-for-byte unchanged. Copies the@tableshape, not just the grammar: a distinctQualifiedForeignKeyName(Optional<String> schema, String name)carrier plusparseQualifiedForeignKeyNameparsed once at the author-string entry points (so the two non-author-facing internal callers keep the two-argfindForeignKeyand stay visibly exempt from the grammar), and a schema-scopedfindForeignKey(name, source, schema)overload. Scoping precedence is qualifier-hard / source-scope-soft: the qualifier filters onfk.getTable().getSchema().getName()(case-insensitive, exactly as two-argfindTablematches its schema half) and an author scope that eliminates every candidate isNotInCatalogfor 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 existingdistinct-then-Ambiguousending rather than a silent first-hit. The "does not connect" connection check was lifted out of the{key:}path element into one sharedforeignKeyConnectionRejectionenforcer firing uniformly at both author-facing sites (the path element and the explicit record-FK siteresolveRecordFkTargetColumns), 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 LSPDiagnostics.validateCatalogFkmirror 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 duplicatedup_gizmo_fkon twomultischema_atables pins that(schema, name)is not unique, and theinit.sqlcomment 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-schemagadget_widget_id_fkeyand the within-schema duplicate ambiguity, plus the parser battery and the tworesolveRecordFkTargetColumnsarms), 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: theReferenceElement.keydirective doc string and the@reference/@referenceForreference pages document theschema.constraintform. 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 Revieweed4ac0; Spec76e8cdf, Ready26ff511, In Progressae67d35): A payload-returning@mutation(typeName: DELETE, table:)field rejected with the generic "return type not yet supported" the moment the deprecated@tablemoved 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 grounderRecordBindingResolver.groundDmlMutationFieldread the write target only off the single@table-bearing input argument, so a field-derived DELETE grounded noProducerBinding.DmlEmitted, the payload never registered as a producer-backed carrier, and the return classified down theScalarReturnTypearm 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 helperMutationInputResolver.resolveDmlWriteTableRef(@mutation(table:)preferred on a verb inTABLE_ARG_SUPPORTED_VERBS, else the input’s@table, resolved through the catalog), consumed by both the grounder and the classify-timeFieldBuilder.resolveDeleteWriteTarget, so a groundedDmlEmittedand 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, andgraphitron:validatealike; arrival cardinality lifts from the field’s single input-object argument (the@tableinput 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) remainresolveDeleteWriteTarget’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),DmlBulkMutationsExecutionTestround-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-review8c308cf, rework0fccc23; In Review7d62453/0933b96, rework requesteda68a8e7): Multi-tenant fan-out:directive @tenantFanOut on FIELD_DEFINITIONruns 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 flatGraphitronRuntimeconstructor scalars,fanOutConcurrencydefault 8 and a 10s deadline, plus an optional consumerExecutor), joins with the deadline, and reports a sealed per-tenantOutcometaxonomy (Success/Failed/TimedOut), policy-neutral about partial failure; fetchers stay synchronous, workers resolve connections only throughdslFor(key), and the thread-safety rework (concurrentpinnedByTenantwith per-key single acquisition, straggler quarantine routing possibly-live connections throughConnection.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-bindingFanOutarm to the sealedTenantBindingwith 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 remainingTenantBindingconsumer 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-elementDataFetcherResultlocalContext (soInheritedchildren and@splitQueryloader-name partitioning compose unchanged), and collapses failed tenants into one appendednullelement each plus a path-bearing redacted error withextensions.classification(TenantFanOutFailed/TenantFanOutTimedOut); both factory forms gain a dedicatedCollection<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@tenantFanOutreference 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,@splitQueryper-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 Review564c561; Spec7e8ec4a`+f9267ce, Ready `9438f81, In Progress01b7a2e): 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" indevelopment-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; `BuildContextrejoined 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-dbwith the{@link}reference gate andRoadmapReferenceGuardTestactive. -
R51 (
b8be7ec, In Reviewa39df32; Speced3a88a`+94a6d25, Ready `7299728, In Progresseccc4fc): MergedChildField.PropertyFieldandChildField.RecordFieldinto one record-read leaf,ChildField.RecordReadField, under the fresh name that forces every switch arm andinstanceofthrough the compiler. The two leaves were one leaf wearing two names (samesourcefact, same empty operation set, same read mechanism), and the emit layer proved it: bothbindarms dispatched to a sharedpropertyOrRecordBinding, four more two-arminstanceofchains extracted the same slots from either leaf, bothTypeFetcherGeneratorarms were identical no-ops,validatePropertyFieldwas an empty method pointing at a nearly-emptyvalidateRecordField, and the LSP projection already merged them onto oneFieldClassification.RecordOrPropertylabel with byte-identical arm bodies. What actually differed was carried facts, not identity, so the nullablecolumnName/column/accessortriple became one non-null sealed component,ValueLocator, with four arms per the real populations:TypedColumn(ColumnRef)(typedTables.X.COLread off a table-record parent),JavaAccessor(AccessorResolution.Resolved)(class-backed parent),ByName(String)(untypedDSL.field(name)read on aJooqRecordCarrier), andDefaultRead(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 overloadedByName). Three model changes rode along, each deliberate and challenged rather than assumed.target()now deriveslistOrSingle(returnType.wrapper(), Field)unconditionally: the scalar path gains theReturnTypeRef.ScalarReturnTypeit did not carry, so a list-shaped scalar such as an@errortype’spath: [String!]!obeys the SDL-list mirror, theerror-field/error-typecorpus rows fliptarget: SingletoList, and the merged leaf exitsWrapperAlgebraTest’s `CARDINALITY_NOT_MODELEDexemption (now four members, the column family andErrorsField, 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 andObjecton the other; the change is diagnostic-visible throughMultiProducerDomainTypeDisagreement, 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 letsFetcherEmitter.recordReadBindingandinlineSuccessReadbecome 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 asinstanceofcross-checks against nullable slots. Corollary cleanups:isEnvDependentAccessorRead’s `JooqRecordCarrierearly-return deleted as structurally impossible once the accessor lives on an arm, andinlineSuccessRead’s broad defensive terminal `thrownarrowed to a singleDefaultReadarm. Implementer decisions beyond the spec:DefaultReadregistersPropertyDataFetcher.fetching(name)explicitly (mirroring theErrorsFieldPayloadAccessorarm) so "graphql-java’s default machinery applies" is an emitted value rather than absence-of-registration, and joinsresolvesViaPropertyDataFetcher, making theDefaultRead-under-Outcomecorner a validate-time rejection; and the LSPRecordOrPropertyprojection now derives per locator arm, so aJavaAccessorread projects its accessor member name with a nullcolumnNameinstead of faking a "Column:" hover label. Coverage:RecordReadFieldValidationTest(unit tier, one positive and one negative case per constrained arm plus the unconstrainedDefaultRead),RecordFieldAccessorValidationTestrenamed toRecordReadAccessorValidationTestwith the two single-purpose validation tests collapsed into it, and corpus rows plus thegraphitron-sakila-examplecompile as the right tiers for a representational change (no per-arm unit tests, no code-string assertions on generated bodies).GeneratorCoverageTest’s two `IMPLEMENTED_LEAVESentries became one; generated sakila output byte-identical;supported-schema-shapes.adocandinference-axis-coverage.adocregenerated. Deliberately out of scope: foldingNestingField’s passthrough read and `ErrorsField’s `TransportlocalContext arm-set ontoValueLocator(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 theDefaultRead-under-Outcomerejection, whose absence leavesinlineSuccessRead’s unreachability claim unpinned). Independent-session In Review → Done review; full reactor green under `-Plocal-db. -
R527 (
4df1008`+27eec25, rework `db67c3e; Spec29feac9`+01dec0f, Ready `c491c60, In Progressd6bc2e2, In Reviewfa12d41, rework requested054959e, re-reviewfc38a2c): 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 inGraphQLQueryTestwere 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 trailingfromArraykey decode resetting the touched flag of every null-valued column record-wide (Record.from()null-skip semantics) and erasing the earlierNULLwrite.customerUpsert_explicitNullNestedLeaf_noIdentityDecode_writesNullpins the isolated counterpart (no decode, theNULLwrite survives), theopenDescent/emitColumnBinding/emitKeyDecodejavadoc 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 forClassNotFound`" 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, fromScalarTypeCompletionsingraphitron-lsptoScalarTypeResolver.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-timeIllegalStateExceptionguards asserting the opposite of the model doc, and twoDSL.noCondition()fallbacks whose comments claimed to be "runtime-throwing" stubs surfacing at execution whennoCondition()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 throughFkJoinResolutionbefore any pair list exists, soOn.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 therule7/rule8locals tohandlerCardinality/duplicateMatchCriteria; "Rule 7"/"Rule 8" survive in prose deliberately becauseErrorChannelWalkerError.ChannelRuleViolation.ruleNumberrenders 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 inPositions) and the R496/R521-owned sites are deferred by charter. Claim 5 retires the "nothing machine-enforces this clause" admission onSource.OnlyChildwithArrivalUniformEmitPinTest, a guarded census (vacuity floor plus an anti-over-deletion carve-out pinning thatChildField#sourcestill mints both arms) asserting zeroOnlyChildtokens 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 fullgraphitron*glob, leaving two dead §4 cites ingraphitron-sakila-service’s `FilmLookupNotFoundException, repointed in prose toFieldBuilder.checkDeclaredCheckedExceptions(a{@link}cannot resolve from that module). Independent-session In Review → Done review across both rounds; full reactor green under-Plocal-dbwith the{@link}reference gate andRoadmapReferenceGuardTestactive. -
R519 (Phase A
8c32c37`+7771d6e, Phase B `7eef0dd, In Review67df32a; Specd856b77, Readyd55cdf9, In Progress67a8308): Retired@tableonINPUT_OBJECTand deleted theGraphitronType.TableInputTypebridge, 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_DEFINITIONprecedent): the SDL declaration keepsINPUT_OBJECTso graphql-java does not fail with a locationless grammar error, andTypeBuilder.buildInputTyperejects any application at classify time with a migration message carrying the per-verb guidance the retiredemitTableOnInputDeprecationWarningsused 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.singleTableInputTypewas 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, whilerequireDmlDataTableMatchesInputTablegoes as structurally tautological once rung 1 drives. The query-sideTableInputArg-from-the-input’s-own-table arm retires (the carrier itself survives, still built field-relatively), andDmlWalkerInputArgResolutioncollapses toRawArg | Rejectedwith the stale “@mutation` fields only accept@tableinput arguments" message rewritten. TheInputBeanResolverjOOQ-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.TableInputand its arms inTypeContext,LspClassificationLabels,DeclarationHovers,InferredDirectiveArgs,SchemaView, andEdgeProducer; the type-declaration hover survives with more honest content throughPojoInput.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) replacesTableOnInputDeprecationWarningTest; two newGraphitronSchemaBuilderTestpins 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.adocregenerated without the leaf. Out of scope and carried by R520: the changelog migration note and dropping@tablefrom the LSP’sINPUT_OBJECTcompletion list, still real because the location stays declared. Independent-session In Review → Done review, which additionally swept a staleGraphQLQueryTestcomment citing four retired symbols; full reactor green under-Plocal-dbincluding the PostgreSQL execution tier, the docs render, and the{@link}reference gate. -
R535 (
19696a3, reworks0761194`+41d3ee0; Spec `306f233`+f0d030d, Ready `06371d8, In Progressa922982, In Review1e2c4e5, rework requested4e09c87`+fdbee5c): Removed the `@tableMethoddirective 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’sREJECTED_ON_USEhalf-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.graphqlsis untouched by design: its declaration is what routes the directive into the Legacy-only section. Gone with it:TableMethodDirectiveResolver, bothGraphitronFieldleaves (QueryField.QueryTableMethodTableField,ChildField.TableMethodField), the DTO-parent batched arm inFieldBuilder,TableExpr.MethodCall(the seal reverts toCatalog | RoutineCall, andJoinPathEmitter.emitTableExpressioncollapses to one overload now that no node needs the call context, itsMethodCallwiring-bug guard deleted rather than lifted),ProducerBinding.RootTableMethod,FieldClassification.TableMethod, both fetcher builders with theirIMPLEMENTED_LEAVESrows, andbuildTableMethodParentCorrelation. Two shapes the plan did not anticipate, both found by the compiler:FieldClassification.QueryTableMethodis renamed rather than deleted, because@routinereads and routine writes both project onto it and after removal it is the routine projection and nothing else, so it becomesRoutineBackedmirroring the siblingServiceBacked(a user-visible rename, since the projection-record simple name reaches hover headers and inlay labels); andServiceCatalog.reflectTableMethodstays under its directive-derived name because@conditionreflects through it and it is the sole producer of the survivingMethodRef.StaticOnly, but with every surviving caller now@condition, itsTableSlotPolicyenum,FORBIDDENarm,expectedReturnClassparameter, strictClassName.equalsreturn check, andReflectionError.ReturnContextare all unreachable and deleted, collapsingReturnTypeMismatchto a single message form. LSP and MCP took structural deletions, not mention sweeps: theDirectivePolicybinding entry, theLspVocabularyBehaviorpair, and the seal-arm rows inDeclTarget,LspClassificationLabels,DeclarationHovers,SchemaView, andEdgeProducer.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 dropstableMethodfromWITHHELD_FROM_V1(down tosourceRow,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 `@routinewhere the method wrapped a database function and@conditionwhere 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, theGraphitronSchemaBuilderTestenum rows, and theClassifiedCorpus"table-method"example; the"record-method"example is edited rather than deleted (the@lookupKeyhalf 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@tableMethodunsupportedPathruntime throw to build time, discharged outright), R240 renarrowed onto the survivingMethodRef.StaticOnlyproducers, 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 sameArgCallEmitterjavadoc block: the first because the removal orphaned the four-argbuildMethodBackedCallArgsoverload (its only callers were the deletedTableExpr.MethodCallarm and the deleted fetcher builders) and the sweep’s find-and-replace had produced "neither@servicenor@service`"; the second because the replacement text attributed one caller to a root service fetcher when both surviving callers are `ChildFieldarms (theServiceTableFieldlift-back call and theServiceRecordFieldrows-method body) and neither passes thenullsourcesExpressionthe 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 andRoadmapReferenceGuardTestactive. -
R516 (
cc270f1`+ffce130, reworks `b811bdb`+25d7e17`+2b64dc4`+696b939; In Progress `62f3d70, In Review69004d7, rework requested7bc3f2d`+f8d2668`+83318db`+9a36bf6, re-reviews `f377c2c`+27ca485`+08e6957): Narrowed theSourceKey.Wrap.TableRecordservice-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.collectRequiredProjectiondrops thereservedFullRowspecial case soWrap.TableRecordfalls into the samebaseColumnsarm asWrap.Row/Wrap.Record(theRequiredProjectionrecord itself retires, one axis being aList<ColumnRef>wearing a name), the__src_<col>__reserved-alias scheme is deleted, andGeneratorUtils.buildKeyExtraction’s runtime `instanceofparent-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. ASet<XRecord>/List<XRecord>Sources shape on a PK-less parent table now fails classification with a dedicatedSourcesOnPkLessParentrejection naming the table (child coordinates only; root keeps its own arms), pinned at the pipeline tier with three controls.FilmService.titleTitlecaseandCityService.cityUppercasewere rewritten onto the idiomatic single-statement jOOQ batch fetch (selectFrom(..).where(PK.in(ids)).fetchMap(..)) as the manual’s canonical pair, andhandle-services.adocteaches 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 intoRetiredVocabularyGuardTest’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 throwsIllegalArgumentExceptionwhileRecord.into(Field...)yieldsnull; a fresh typed record carries its complete row type with values unset, so reading an unset column off the key returnsnulland never throws, whatever the client selected). Test surface:ServiceParentTableRecordKeyExtractionTestand the federation test asserting the reverted behavior deleted,entities_tableRecordServiceChildOnly_serviceFetchedColumnResolvesNonNulladded for the corrected contract under_entitiesdispatch,PkLessParentServiceSourcesRejectionTestadded,DmlBulkMutationsExecutionTestre-anchored off the reserved alias,TypeSpecAssertions.appendsFullParentRowdeleted andserviceChildKeyExtractionForksOnTypedRecordbecameserviceChildKeyExtractionIsUnconditional(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; Specba0e54b, Spec revisionb27d7a9, Ready2e5e66e, In Progressb9abcc1, In Reviewa570739): Measured the test pyramid: JaCoCo coverage instrumentation as an opt-in-Pcoverageprofile in the parent pom (prepare-agent withappend=falseat 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 ajacoco.outputDirectoryproperty indirection repairing a falsified spec premise: the report mojo bindsjacoco.destFile/jacoco.dataFileas user properties but givesoutputDirectoryno property expression, so without the indirection-Djacoco.outputDirectorywas silently ignored and the per-tier report overwrote the combined one. Report surface:SourceCoverageReport(roadmap-tool, in-memory DuckDB over the per-modulejacoco.csvglobs, followingLeafCoverageReport’s shape) renders `roadmap/source-coverage.adocwith four tables: per-module line/branch/method,graphitronper-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--verifydrift mode either);render-adocsynthesizes a stub when it is absent and CI republishes the real page per trunk push. The tier vocabulary hoisted intoTierVocabularysoLeafCoverageReportandSourceCoverageReportcannot 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 theunitcolumn by design.CoverageAgentWiringCheckjoins 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 distinctdestFile, or aforkCountother than 1, since both wiring breaks are silent under coverage and invisible without it. CI: the build job gains-Pcoverageon every run (PRs continuously exercise the wiring, roughly 20-30 s agent overhead on a 139 s build) and flipsverifytoinstallso the two trunk-gated per-tiergraphitronruns (-Dgroups=unit/pipeline, each with-Dleaf-coverage.skipto 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:
7d87b051,74887652,0e7fcd23.1,334098d3.2,bd1c7123.3,ed9699d`+3febf31`+c0a72643b,e9529604,8a13616`+b41fcdb`+59b87385a,4d0792b`+6f589ba` 5b,e4ea3bf5c,26fa34e5d,765e6435e,c26966c5f,efafa5d6,761ee16`+4906a0a`+65458b97a,bcd463f7b,dfb06d68; Spec648563b, Ready88f2112, In Progressb9f5b9f, In Reviewbac46e8): 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: thecommand/plan/renderpackage triangle withPackageImportDirectionTestenforcing it,EmitPlanas the core step producing the global, type-keyed, coordinate-keyed launcher, projection, condition-consuming, fetcher-edge and connection-synthesis relations, one$projectmethod per projection unit (nesting types promoted to anchor-prefixed units), every contribution gated on client selection (over-projection ended,ParentProjectionContainmentCheckand the required-projection walk deleted, the one deliberate SQL re-baseline), the fact-visitor engine with the pagination beachhead, the DML reentry fold retiringMethodCommandRegistry, 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 (theconnectionName:plus@asFacetrejection 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 leafinstanceof104 to 71,case89 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,TypeFetcherGenerator7,102 to 5,998 lines,methodgraphpackage retired; honest counterpoints recorded (FieldBuildergrew 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 ($projectvocabulary in five sakila-example test files' prose, a stalePROJECTED_LEAVEScomment inGraphitronSchemaValidator, twoCarrierRewritetest 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 mintedOperationMemberRelation(post-walk trigger-fact fold) becomes the production behindGraphitronSchema.operationMembersOfover 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;Operationre-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.nameand 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/@externalFieldrejected viaExternalRef.lookupError, path-step@conditionblamed a missing method, and field / argument / input-field@conditionreturned null, whichConditionResolverreads as "directive absent" and which silently dropped the predicate from the generated SQL at exit code 0. Droppingnamefrominput ExternalCodeReferenceindirectives.graphqlsmakes the site a schema-load failure ("Fields ['name'] not present in type 'ExternalCodeReference'") and takes every reader arm,ExternalRef.lookupErrorand its two resolver consumers, theRewriteContextandCompletionDatacomponents,NamedReferenceBindingplustoNamedReferenceMap, and the LSP’svalidateLegacyNameLeavesdiagnostic andname-to-classNamequick-fix with it;ARG_NAMEstays, being also@field(name:)and@table(name:).SdlActionskeeps the generic action framework with an empty registry, andCodeActionsTestmoved into the package to drive the three activation points and the message branches through a new package-privateCodeActions.computeseam 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 aRemovedtable, andmigrating-from-legacy.adocfiles 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 survivingnamedReferencesmention 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 genericdescendUnknownArgsdiagnostic the LSP now falls back on reportsWarningfor what is a hard build failure, inverting the editor/build strictness relationship) and R579 (FieldBuilder.parseExternalRef’s `parentTypeNameparameter 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
Pivotmember, and the surviving leaves hold the reconstruction keyleaf = 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, rework067d749; Backlog → In Review77e8adc, approval reverted2178df3`+bf3d987, In Review `ffece00):LspTrace, a default-off span seam in a newno.sikt.graphitron.lsp.tracepackage 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 forSystem.error a file named bygraphitron.lsp.trace.file, because the stdio launcher speaks JSON-RPC onSystem.outwhere a console appender would write, and because that same deployment usually carriesslf4j-apiwith no backend bound, so a logger-based seam would emit nothing; the sink is captured at class initialisation so a laterSystem.setErrcannot redirect trace lines onto the protocol stream, andsinkForTestingstays 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, withint/longdetailoverloads 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, thewithView/withAllViewslock-held regions scoped to the lock so the duration reads as lock-wait, the mutate/notify split inenqueueAndNotify, the per-edit reparse and type-index refresh, the diagnostics document walk and its whole-report validator projection separately, andLspVocabulary.load. First measurement already corrected a ranked hypothesis: the dominant per-edit cost is the full-treeTypeNameswalk 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;$/setTracemapped ontosetEnabledso 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 sendtrace: offas boilerplate and honouring it would silence a deliberately-setgraphitron.lsp.tracebefore a single phase had been traced; and package-privateslowMsForTesting,openSinkandenabledFromseams making theSLOWtag, the file sink with its fallback-to-stderr arm, and theGRAPHITRON_LSP_TRACEarm assertable at all, each previously resolved once into astatic finalat class initialisation and reachable by no test. Two forks decided as rejections with the reasoning recorded rather than left silent: routing output throughwindow/logMessagewould 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 akill, 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:LspTraceTest7 to 14,GraphitronLanguageServerTest3 to 6;dev-loop-internals.adocgained the file-sink recommendation, the$/setTraceasymmetry, and the subtraction that gets at per-directive cost (diagnostics.computeminusdiagnostics.validatorReport, overdirectives=) 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, 467graphitron-lsptests; 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:setEnabledis a JVM-global flip andgraphitron:devcan serve several connections in one JVM, so one client’s$/setTrace offsilences 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
Operationseal RETIRES.@classified(operation:)re-grained to the requiredoperations: [Member!]!member-list assertion (sorted arm-token multiset, the@commitsgrammar applied to the member relation; produced side read throughGraphitronSchema.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.summaryArmOfwith its documentedNest/Fetchempty-set fiction, the SDLOperationenum, and the 17-armOperationseal itself deleted (zero code readers remained; the corpus vocabulary is theOperationMemberseal through the preludeMembermirror).MEMBER_ARMSre-sources the coverage obligation (domain = the 21 sealed member leaves, covered = declared-and-agreeing rows, six re-keyed exemptions), the newpolymorphic-filterexample reachesCondition.OnParticipantwith two rows (the multiset voice’s demonstration), each declared list is fenced production-independently againstDECLARED_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
sealedLeavesdomains,@ProjectionForpins 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 scatteredinstanceof Keyedspellings (five presence-only sites migrated; a new arm now forces one decision instead of silently answering "not keyed" everywhere), theFieldClassificationrecord javadocs dropped their hand-written leaf censuses outright (the total projection switch and the@ProjectionForpins 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,assertHonouredintersects covered with domain once for honest counts on every row, the emptied stub chain lost its would-NPEstubbedErrorhelper while the gate stays armed, andUPSERT_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.HarnessSingleCatalogand 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,MutationServiceTableFieldamong them; the missing--verifygate that let a partial-trace regeneration commit silently filed as R590),code-generation-triggers.adocswept onto the keyed-lookup-arm vocabulary (the lookup rows across five tables plus theTableTargetFieldcensus, now 4 variants, with the lookup emission pointed atrender/LookupRows; two directive-effect rows beyond the review’s enumeration caught in the same sweep), the fourLookupValuesJoinEmittertest-prose sites re-pointed atrender/LookupRowsincluding the dangling test-source{@link}, and therequireNoGeneratedFilterOnLookupretired-vocabulary row reworded as the re-grain it was (the method is live inConditionCommands; 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 → Ready34ee0be, In Progress08271b8, In Reviewc553838): Pinned thetypeIdaxis ofBuildContext.resolveTargetKeys’ name-first read on both jOOQ-record decode arms. Coverage only, no production change: the resolver already prefers the `NodeIndexby-name entry over the backing table’sKjerneJooqGeneratormetadata, so a@nodeId(typeName:)target takes the named type’s reconciled@nodeanswer, but only thekeyColumnsaxis 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 atypeIdregression would have shipped silently, and a wrong prefix is not cosmetic: it becomes the emitteddecodeValues(typeId, nodeId)argument, the encoder returnsnullon a mismatch, and the generated helper then rejects every well-formed client id at runtime on an input that builds green. The six pre-existingtypeIdassertions were blind because no fixture reachable from them paired a metadata-carrying table with a differing SDLtypeId(filmandemailcarry no__NODE_TYPE_ID, andfilm_actor’s metadata `typeIdequals its type name). Three cases close it, all model-level assertions on the resolvedCallSiteExtraction, none on generated bodies:NodeIdRecordInputBeanPipelineTest.sdlTypeIdOverridingMetadata_readsTheNamedNodesTypeId_notTheTablesMetadata(@node(typeId: "FA46")overfilm_actor) and.siblingNodeTypesOverOneTable_readTheNamedOnesTypeId(two@nodetypes 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 theRecordKeyDecodearm inJooqRecordServiceParamPipelineTest, 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, withresolveTargetKeysreverted to metadata-first across the whole module: 4 failures of 3150, the pre-existingkeyColumnspin 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, reworkbf4971b; Backlog → Spec012203e, Spec → Readyefd3700, In Progress79f2598, In Review → Ready281cf33, body collapsecbf0586):InputFieldResolution.Unresolvedcarried(fieldName, lookupColumn, reason)where every sibling builder-step result carries a typedRejection, 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/@lookupKeycauses converged from five spellings ontodirectiveConflictso each cause has one identity carrying its directive name, the FK-target key mismatch onto theRejection.deferredarm its own message asked for,buildInputFieldCondition’s `List<String>out-param onto a typedInputFieldConditionFailureaccumulator, 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 `ValidationErrorat the input field carrying it viaBuildContext.mintInputFieldFailures, the consuming coordinate keeps a single rejection stating the consequence rendered fromInputFieldsResolution.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 aLinkedHashSet, which also deletedFieldBuilder’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 twocollectInputFieldRejections(...).getFirst()write-target paths stopped dropping every mirrored rejection after the first;DirectiveConflict.directivesgained 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-TableFieldsite stopped listing the absentsplitQuery; the@referencecolumn-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 stayStructuralon 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: typingParsedPath(its own item; this one wraps it at one boundary and deletes the wrap when it lands), the other three carriersR58TypedRejectionPipelineTestnames, 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@conditionfailure 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.bf4971bcarried the declaring type onInputFieldConditionFailureand swept the prose. Second gate: full reactor green under-Plocal-db, 14/14 modules, 3197graphitrontests, 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 (nodocs/changes). TheDirectiveConflict.directivescontract 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 → Readybbca905, In Progress2828bf9, In Review354fb6f):AdocXrefAnchorCheck, acheck-adoc-xrefsgate on cross-filexref:<file>.adoc#anchorreferences 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, whichfailIf severity=WARNdoes 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 becausedocs/pom.xmlsetsidprefixempty andidseparatorto-. 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 againsttarget/stagingrather than the source tree, invertingAdocMarkdownTableCheck’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.adoconly after rendering. It binds in the docs module’s base build afterrender-roadmap-adoc, not in thedocsprofile, so-P!docsis 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.maskInertholds 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 ofInertSpans.scaninto a sharedBlockContext. 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.adocgains 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 observingBUILD FAILUREunder-P!docswith 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, correctionsee587c7+ffe31f7; five-slice plandc29447, Spec → Readyb84572d, In Reviewd87ec18; the module, DDL, capture loads and agreement driver shipped before the item reopened at its first In Review gate): thegraphitron-modelfact store, completed across five slices after the reopen. Slice 1 fixed the reopen blocker: both production call sites handed capture the registryKeyNodeSynthesiserhad already rewritten in place, so the federation walk macro was inert, the synthesized@keytranscribed as authored and unlocated, and the authored-picture anti-join broke;AttributedRegistrynow carriespreSynthesisRegistry(), areadOnly()snapshot cut where the loading rewrites end,TagApplier/DescriptionNoteAppliersit above the cut by decision (their output is in the emitted schema the store owes a round trip of) withKeyNodeSynthesisermoved 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 renamedcatalog_/extension_tosql_/jvm_(families named for whose vocabulary a row is written in),java_nametojooq_name,extension_scalar_constanttojvm_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 staletarget/classescopy shadowed a DDL edit at codegen. Slice 3 reshaped constraints as the catalog does:sql_constraintsupertype discriminated by type plussql_constraint_column/sql_primary_key/sql_referential_constraint, capture readingJooqCatalogdirectly instead of theCatalogFactsprojection whose narrowings (primary-key split, candidate-key dedup, missing referenced-constraint name) were baked into the store;sql_column.ordinalcomes from declaration-orderedTable.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@scalarTypeconstant generated fine and red-squiggled in the editor;jvm_method.descriptoris the real JVM descriptor,store_sourcerecords 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,StoreRefreshkeeps partitions whose source still hashes to whatstore_sourcerecorded (retention enforced by seedingFactSink.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 (nodocs/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 passes4bda96a+6b06c77+3414c49, Spec → Readyd5efd16, In Review266b10a): 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 isAbstractRewriteMojo.decodeDependencyVersions, turning both artifact sets into(coordinate, version-string)pairs, becauseArtifactandMavenProjectare external untyped input that must not cross into the generator; the predicate needs the version decomposed rather than ordered, which is plain-string work, soComparableVersionis declined and the(major, minor)integer compare gets3.9versus3.20right 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 oftest, becauseResolutionScope.COMPILEandTESTdiffer by two scopes and excludingtestalone would leave a runtime-scoped coordinate observed underdevand silent undergenerate, the same project saying two different things depending on which goal ran. It rides theBuildWarningchannel as twoSource.CODEGENlint rules with anulllocation, 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 minor0rather 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, theruntimerow load-bearing), a fires-then-suppressed case inLintSuppressionPipelineTest,basic-generateasserting an up-to-date consumer and one carrying no graphql-java are both silent, and a newdependency-version-lagIT declaring jOOQ3.19.24directly and asserting the nudge fires. The four<sessionState>-flavouredSource.CODEGENdocumentation sites are widened to what the axis actually partitions, a whole-build fact with no SDL coordinate folded in at report assembly, anddocs/dependencies.adocgains 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 newdocs/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 the3.20.11reference resolved off the live plugin realm. One coverage gap filed as R611 rather than held against the gate: the watched coordinateorg.jooq:jooqis 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 → Specda4dff4, spec revisioncdbf9bb, Spec → Ready7794f24, Ready → In Progress6423076, In Review9d5da48): 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, anddocs/dependencies.adochad 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. AWatchedDependencyconstant 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 becauseorg.jooq.protracks 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 swallowingjooq-codegenand its commercial twins.DependencyVersions.observed()becomes aMap<WatchedDependency, List<ObservedVersion>>where the newObservedVersionrecord pairs the resolved coordinate with the resolved version, andAbstractRewriteMojo.observedVersionsOfappends every match rather than collapsing on first occurrence, because Maven mediates per coordinate and not per library, so the oldputIfAbsent(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 ownreferenceVersionsOf, 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 inDependencyVersionWarnings.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, andMinorLinebecameComparableso 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 newLintRule; the suppression id staysjooq-version-lagso a consumer who already silenced the nudge keeps it silenced across an edition switch, andLintRuleRegistryCoverageTest’s hard-listed `Source.CODEGENset 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-codegenunmatched under all four group-id shapes, a group id merely sharing theorg.jooqnamespace 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 nameorg.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 onorg.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, withDependencyVersionDecodeTest19/19,DependencyVersionWarningsTest23/23 andLintSuppressionPipelineTest6/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.versionsOfand theWatchedDependency.coordinate()/groupId()/artifactId()accessors have zero live references, and the survivingputIfAbsentis 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 thedependency-version-laginvoker’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 enumeratesorg.jooq.pro,org.jooq.pro-java-<n>andorg.jooq.trialbut omitsorg.jooq.trial-java-<n>, which the code covers andDependencyVersionDecodeTestpins. -
R618 (
a82f59athe carrier end to end,b2b065fexecution proof for both null-data outcomes,726586auser documentation and coverage refresh,2929b84the fact-minted write membership; Spec → Readyc2c234d, Ready → In Progress456ecc3, In Review58e0949): a@routinewrite onMutationcould only return the terminus@tabletype through a@routine+@referencechain, and a hop-less@routinelanded the single-node typedDeferred, 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 typederrorslist was to abandon@routineand 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 newMutationField.MutationRoutineWriteRecordFieldleaf whose one legal reference seat is the payload’s data field, so@routine+@referencewith a carrier return rejects as a typedRejection.directiveConflictnaming 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 totalEmittedCarrierBinding.correlationColumns()on every carrier family while the pairs' source side, the routine-only fact step 1 projects, travels on theProducerBinding.RoutineEmittedarm 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 falsecondition: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:EmittedCarrierBindingover the three arms, taken, because theactiveChannelgate inFieldBuilder.transportForParentwas a hand-maintained boolean disjunction whose miss is silent (the carrier’serrorsfield would bindTransport.PayloadAccessor, the one transport a directiveless structural carrier cannot serve, whileselectErrorsTransportnever 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 asroadmap/emitted-carrier-binding-consumer-consolidation.md, because the two real diagnostics have fixture-pinned wording and unifying them would drag the untouched DML and@serviceemit paths into this item’s acceptance surface.CarrierFamily.ROUTINEearns 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, andRoutineDirectiveResolversplit a node-only resolution out for the carrier seat with the chain path’sresolvebehaviour-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 intoerrorsinstead 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 witherrorsempty, 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 anAuthorErrorat 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 caseLeafReconstructionKeyTest’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 inGraphitronSchemaBuilderTest(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,@referenceon the data field landing the pointedDeferred, the record-element and ID-element refusals, D7, the narrowed single-node deferral) plus theRoutineBackedprojection and a@classifiedcorpus 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.rentFilmPayloadover the shippedrent_filmFK-violation path and (b) as a genuine RLS fixture (secure_noteplus aSECURITY DEFINER create_secure_note, ininit.sqlso 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.adocgains the carrier as a second admitted return shape headlined by the one-sentence rule (with@referencethe field returns the table the chain reaches, without it a payload whose data field declares its own path), anderror-channel.adocstopped claiming@serviceis the only upstream of an errors channel, which had been untrue since DML carriers started bindingErrorChannel.LocalContext. Deliberately out of scope, each landing as a typedDeferredpointing 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),@routinecarriers onQuery, 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 undermvn 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 andSingleRecordPayloadPipelineTestalready 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: theGraphitronSchemaValidatorarm 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 throughvalidateUnclassifiedField, so a live arm would be unreachable code) and matches both the direct-return sibling andRecordCompositeField; androutineEmittedMemo’s first-producer-wins `putIfAbsentmatches 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+463dd0c97on branchgraphitron-9-head; Spec → Done at user direction, the intermediate gates bypassed on explicit instruction, with the human user as reviewing party): splitGeneratedExceptionToErrorMappingProviderunder 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 declaredList.of(a unique list keeps its operation-derived name; a shared one is namedshared<Handler>List<n>). Part 2 splits emission intoinitMappings<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-localwidepenalty), and the split is conditional on the estimate, so under-budget schemas keep byte-identical v9.3.2 output and thedefaultCase/noValidationgolden files did not regenerate. The spec’s three coverage asks all landed:ProviderScaleTestgenerates 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 ofcode too large, making compilation itself the assertion);ProviderChunkingTestpins the split mechanics under an injected minimal budget;sharedListspins 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;multipleInOneResponseandbothGenericAndDatabaseInMultipleErrorsForOneResponselost their illegal-code caveats), and theExceptionToErrorMapping.equals-omits-handlerpair the spec recorded as unreachable was shown reachable (the DATABASE handler’sclassNamedefault collides with a GENERIC handler namingorg.jooq.exception.DataAccessExceptionwhen all other fields match) and is defused by keying the generator’s memo on (handler, mapping), pinned byequalMappingsAcrossHandlerTypes, 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 (
01d6dd4the seam,9c9d824coverage across four tiers and the dot-path contract authored once,a7394c1gate rework; Spec → Readya9dda9c, Ready → In Progressc81464c, In Review2322a09, rework requestede193389, rework In Review58a1a73):argMappinglooked like one authoring surface and behaved like four, so a dot-path that worked on@servicewas told on@routinethat 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, soArgBindingMap.of(which already implemented them,liftsListand 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.parseArgMappingandofstay two separately callable functions becausecolumnMappingshares the parser but resolves against catalog columns, andBuildContext.readConditionDirectiveparses 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.Resultgains aFailuresub-seal so the four sites that lift both arms with the same site prefix write one arm, whileresolveConditionRefkeeps the records apart and now carries the reason: its slot map is empty, so the shared message renders[]and the clause it adds to theUnknownArgRefarm alone is the only prose that explains it.@routine’s bypass goes: the single-segment guard and the `fieldArgumentNamesmembership check are deleted, argument-sourced parameters resolve againstFieldBuilder.argSlotTypes, and both left-hand-side checks land in the same slice because routing removes thegraphqlArg = param.name()side effect that was the only thing surfacing a left-side typo, and surfacing it only conditionally at that. The leaf gate callsServiceCatalog.argExtraction, written but never called from the routine side, with a routine-specific rejection above it that closes the reported footgun:pParam: inputused to pass validation and emit a cast that threw on the argumentLinkedHashMapat request time.ParamSource.RoutineParamSourcecarries the routine narrowing structurally, deleting five unreachable emitter arms and their factory, and the emitter descends throughArgPathHelperRegistry, a per-host collector modelled onCompositeDecodeHelperRegistrywhose helper takes the root as anObjectparameter so theenvandSelectedFieldforks can each supply their own. Documentation was the surprise the spec review turned up: the dot-path form had been shipping since thePathExprwork described in zero places, so the slice is first authoring rather than convergence, andservice.adoc’s canonical `[[arg-mapping]]section is now the one description with sixxref`s into it, plus a `diagnostics-glossaryentry and a correctedhandle-services.adocclaim 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, whichdevelopment-principles.adocbans at every tier and enforces at exactly that gate, andFieldBuilder.fieldArgumentNameshad 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 theMethodSpec. 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 todevelopment-principles.adocrather than a silent exception. Second pass verified both closures at trunk tip: full reactor green undermvn 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@routinerejection 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 (
5b0614fthe pilot’s substrate shadowed,10f0711the cutover,c41f244the stratum’s DDL and loaders,5abd97bthe aggregate and the widened filters; Spec review passes13ad425+a3eb8b9+c0a4e92, Spec → Ready8526af5, Ready → In Progressd7e853c, In Review18460ac): thediagnosticstool 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-lessdiagnosticunion view and nothing reads a base relation: the store-native pilot (intent_authored_claim_conflict, a derivation view with no writer), therejection_transcription residue and its ordereddirectiveschild,lint_findingin the linter’s vocabulary,build_warning_no_rulefor the advisory arm, and the shippedjavac_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_andwalk_have no referent once the sealed hierarchy and the walk are gone, whilebuild_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 `Rejectionhierarchy 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 aCASEover storedkindpinned by a one-row parity assertion, thelspCode()membership binds through a reflective walk that now compares both readers rather than aCodedRejectioninterface, theStubKeysplit is dropped clean with the absent arm stored as SQLNULL). 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 aswalk_claim_domain_type/walk_claim_domain_field(two per-grain relations rather than the one the spec named, because a single relation needs a NULLfield_nameinside 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 theDERIVEDanchor atAuthoredClaimConflictsTest’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/directivesvocabulary into theRejectionarms, withAuthoredClaim’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, `groupByasGROUP BY, the shared null-safewhereasWHEREwithIS NOT DISTINCT FROMso absence is a value both tools agree on,minCountasHAVING, 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.messageTemplatewas measured and omitted per the Spec review’s stated rule, with the measurement recorded for the gate rather than left to be re-derived: 18Structuralrows 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 aminCountpast every group, the 200-group cardinality cap with balanced elision accounting, the null-keyed absent bucket read back as a drill-down, the canonical sorteddirectivesrender 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-mcpgains its first store dependency, the handle reaching it asGraphitronMcpServer.StoreHandlefromDevMojo’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 undermvn 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 offValidationError’s constructors" so that "no dot-split exists anywhere on this item’s path", and `RejectionFacts.coordinateOfdoes split the coordinate string, becauseValidationError.forFieldtakes an already-joined qualified name and giving it a two-part factory is precisely the carved-out sealed-Coordinateitem; the invariant that actually carried the carve-out (one decode site, in the loader, replaced by a sealed switch whenCoordinatelands, 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 collapsingCompileFacts’ 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 `orderBysilently defaults on any unrecognised value whilewhere’s `severitydoes not lowercase as its sugar twin does, both against the closed vocabulary’s own promise that a bad name fails loudly (R633). -
R630 (
d37e9b6the pipeline overview rewritten,f1b5eea+07ac443the fact-model page and the first axiom,e75d9dcthe forward voice and the reviewer taxonomy,d5d6c32the generated schema reference,c7a7f1ethe drift guard; Spec → Ready32e9bd6, Ready → In Progress83f793b, In Review4ed3e54): 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.adocis rewritten to the shipped shape (capture transcribing totally into the base relations, theintent_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.adocis 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 whosestatus: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 ofdevelopment-principles.adocis restated as "Decide once, at capture; carry the decision and its provenance as facts", paid for insideDocSizeBudgetTest’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<?>orClass<?>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: theprinciples-architectreading 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 theCOMMENT ONtext itself, so the reference cannot drift from the schema by construction. Structure is data too, in a newmeta_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), andmeta_relation_family(theINFORMATION_SCHEMAcensus 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 soStoreRefresh, the partition gate and the warm census need no exemption that could rot, and the uniqueness aPRIMARY KEYwould have given is four bidirectional roster gates inFactSchemaGateTestinstead; 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, soCommentRenderabilityGateTestaccepts 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:StoreCatalogingraphitron-modelis the one answer to "what relations exist", and both the renderer (SchemaReferencePages, homed inroadmap-toolso 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-identifierscloses 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 arelation.columnpair, 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 undermvn 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 toucheddevelopment-principles.adocin 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 (
6b4d1e0one recipe value over a sealed source carrier,0293debthe rest of the config family and the item’s five enforcers; Spec revisionsb1373e9+98b00c1+9281dd8, Spec → Readyf87acb9, Ready → In Progress13f980a, In Reviewb1606f0): the Maven and pom configuration fact family.SchemaRecipe.Bindingwidens to a sealedEntry(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.patterngeneralises tokindplusentry_valueunder a three-value CHECK with no rekey. The row encode and decode live together inStoredRecipe, landing the decode in production.SchemaSourceseals the source into afilearm carrying a normalisedPathand anamedarm carrying a label, decided at mint by the producer that knows:SchemaInput.plainretires,RewriteSchemaLoader.loadnarrows to the file arm,SchemaProblemDiagnostic.normaliseLoadedandSdlFactCapture.regularFileare 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.expandreturns a sealedExpansion(Resolved/NoMatches/ScannerTrouble), dissolvingSchemaInputExpanderinto the mojo-side decode plus the core expansion, withResolved.currencyRelevantMatches()the one place the named-literal exclusion is decided.GraphIdentitynarrows to(name, baseDir)and capture’s subject becomesFactCapture.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 asstore_graph_session_stateplus its arm payloads, with the nested unmount alternation’s opt-out spelled as a missing disconnect row.<supergraph>documented inmojo-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; Specc6b9757, Spec reviewef6961d, Spec → Ready0607a19, Ready → In Progress9fc5666, In Reviewf2da82b): projected@referenceand@externalFieldleaves are admitted under a plain-objectNestingField, 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, becauseProjectionCommands.mintNestedUnitmints a unit per anchor whose$projectreceives the anchor’s own table local andFetcherEmitter.bindreads 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@externalFieldcarrying a@referencepath emitted the helper call with the join path dropped) and a generator crash (aNodeIdEncodeKeysreference carrier reachingcontributionFor’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 `BatchedTableFieldkeyed on an array column mis-batched silently. The nested walk threads the enclosingNestingField’s `TableBoundReturnTypeas an anchor rather than anestedflag, which is what keepsvalidateColumnBackedField’s parent-type gate from false-positiving every plain nested scalar, and `validateNestingFieldsplits 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 incompareNestedFieldsShape, cross-linked from R323), and the@externalFieldparent-table assignability hole found on the way is R646. -
R646 (
e0f3583; Spec8acc81a, Spec revisiona2e4a55, Spec → Readyfa48b0f, Ready → In Progress47fddef, In Reviewef92337, In Review → Readyaa8cbb1, Ready → In Progress18c2a13, In Review3bd27eb):@externalFieldhelper parameters are checked against the parent table.reflectExternalFielddocumented its sole parameter as "assignable from the parent’s jOOQTable<?>class" but only checked that it was some jOOQ table, and theparentTableClassargument was never read, so a helper typed on another table classified clean and emitted<Helper>.<method>(table)into a$projectunit 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’sTableRefand is read as two ordered value comparisons behind the existingTable-subtype gate: table identity (findTableByClasson the parameter class, compared throughTableRef.denotesSameTableAs, so non-catalog supertypes admit and multi-schema same-named tables stay distinct) then record type (a concreteX<R>must name the parent’srecordClass). No live jOOQ handle for the parent crosses intoServiceCatalogorExternalFieldDirectiveResolver; both layers fail open with thegraphitron-sakila-examplerelease-17 compile as the backstop, so neither can produce a false rejection. FourComputedFieldCaserows cover the mismatch, the widenedTable<?>accept, and both sides of the record-type layer;FilmExtensions.titleByName(Table<?>)wired toFilm.titleByNameis the compilation-tier witness that the widened form still emits a well-typed$projectbody. Corpus-neutral. The@conditionanalogue onreflectTableMethodis filed separately as R647. -
R649 (
a896327the phase split, with the gate’s javadoc sweep landing in this entry’s own commit; filed1e8487e, spec bodye746fb1, Backlog → Speca14e800, Spec revisions5c119e7+5661a52, Spec → Readyb4f2f6c, In Review with the implementationa896327, rework requested7a27aa4):ServiceDirectiveResolver.resolvereflected 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:ServiceCataloggainsdecodeServiceMethod(reflection to aServiceSignaturecarrying nojava.lang.reflect.Method, which makes the raw-reflection containment invariant structural rather than commented),reduceClaims(one sealedParamRoleper parameter, so candidacy is decided once as a carried value instead of by three predicates two phases must agree to spell identically), andbindServiceMethod(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 sealedParentContext(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.PkLessParentdissolves intoTableParentwith an empty PK. The preserved return-type regime is a derived axis with an exhaustive switch rather than aRoot || RecordParentdisjunction 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.validateRootInvariantsdissolves, itsParamSource.Sourcesarm retiring as dead code whose message text had already drifted from the live copy, and the fourFieldBuilderarms the resolver now rejects outright become invariant throws naming the classify arm that owns the verdict.ServiceCoordinatePrecedenceTestpins 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;ServiceCatalogTestre-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 undermvn 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:reflectServiceMethodwas deleted with 16 citations outside its own file still naming it, 8 as{@link}and therefore dangling. The javadoc reference gate misses them becauseServiceCatalogis package-private and the gate runs at javadoc’s defaultprotectedvisibility, so the rot was silent, and three of the stale statements misattributed the very responsibility this item moved (TypeFetcherGeneratorstill placed the strict return-type check inreflectServiceMethod;InputBeanResolverandMethodRefstill named it asMethodRef.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 itsResulthalf but not itsScalarhalf, the decodedinferBindingsByTypeoverload dropped theorg.jooq.Tableeligibility filter its reflection-based sibling keeps, and `ServiceCatalogTest’s 41 method names still carry the retired entry point’s prefix. -
R648 (
a033cf1; filed1e8487e, Backlog → Spec92a40fdd, Spec revisionsf800724+7b18bb3+5ca178f+4346e23+183a551, Spec → Ready (deferred on R649)d310b1b, Ready → In Progressa087f1d, In Review with the follow-ups fileda71b047): a child@serviceon a class-backed parent can batch, keyed by the table itsSourceselement 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 newServiceKeySourcebeside the pre-existing table-parent row: the parent’s backing is the declared record (read offenv.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-freeClassAccessorResolver.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, retiringparentPkColumns) and the leaf (as the stored componentChildField.sourceShape()now derives from, closing the latent-wrongTableanswer 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@servicedeclaring noSourcesparameter is rejected at classify time on both parent kinds, which makes both key components non-null by construction, retiresLauncherCommands.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);GraphitronSchemaValidatorstates the batched-child invariant once for both leaves (aSourcesparameter exists, and its columns are the key owner’s primary key). One defect the change made reachable shipped fixed with it:RowsMethodShape.strictPerKeyTyperesolved nested payload classes throughClassName.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.adocgains the class-backed batching subsection and the contract line rewrite;result-types.adocgains 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 flipa71b047, Backlog → Specd4027ae, Spec revisions06a793e, Spec → Ready431bb12, In Review with the implementationf658e09):@sourceRowon a batched child@servicedeclares the batch-key producer a scalar-only class-backed parent cannot infer. R648 left two producer routes (the parent is theSourceselement 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 theSourceselement record itself, site-derived, so the@tablechild’sRowNcontract is untouched. NewServiceKeySource.FromLifterarm carrying aStaticProducerRef(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.@servicenow outranks@sourceRowat the child classify site ; a field carrying both used to route intoSourceRowDirectiveResolverand silently drop the@service; and the@splitQueryredundancy 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 intoLifterMethodResolver, keeping the rejection vocabulary single-sourced; the directive declaration travels as the reduced factSourceRowDeclarationonParentContext.RecordParentrather than threading the SDL node inward. The declaration wins totally over inference (giving the two-accessor ambiguity an exit needing no class edit),JooqRecordCarrierparents 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.adocthree-route rewrite with the lifter example,result-types.adocdecision-row replacement. Independent-session In Review → Done review: full reactor green undermvn 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 inServiceKeySource.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 review836e772, Ready → In Progresse15e1e4, In Reviewcee6a42): 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 atenv.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.DomainReturnTypesplits intoClaim(the three existing arms) andNoClaim, andGraphitronSchemaBuilder.collectDomainReturnTypeConflictscompares 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 throughRowsMethodShape.fromBinaryNameso a nested class is spelledOuter.Nestedonce rather than diverging fromClassName.bestGuess’s `Outer$Nested), and the shared result-return rule (claimForResultReturn) answers by fact rather than by leaf:TableRecordwhere theReturnTypeRef.ResultReturnTypecarries a resolved table (newly threaded fromBuildContext.resolveReturnType), the backing-classPlainwhere only a class grounded,NoClaimwhere neither did. That rule also repairs the root@servicetwins' arm asymmetry:MutationServiceRecordField’s unconditional `TableRecordis confined to the population that resolved a table, andQueryField.QueryServiceRecordFieldanswers 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 toNoClaim, leavingOutputField.OBJECT_CLASSonly aspeelToClassName’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 reachesgraphitron-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+3201386the root half,3d4c78athe child half,5d256cd+18fca6dthe review rework; Ready → In Progressce49fcd, In Review43293b0, In Review → Ready8963951+4d66c8c, In Review76a4ad9):@asConnectionis 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.assemblyemitted two join families and only one of them, the joined-detailLEFT JOIN, is proven single-valued (TypeBuilder.resolveJoinedTableParticipantrequires the detail’s FK columns to be its own primary key); the other, a cross-table participant scalar one@referencehop 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 newtargetAtMostOnePerSourceinvariant 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.discriminatedBranchesmints aSelectTerm.ScalarSubselectcarrying the fixed participant alias and the branch’s discriminator gate) and render through the samePathFragments.scalarInnerSelectthe plain scalar@referencearm calls, a correlated subquery capped.limit(1). The conversion is unconditional rather than pagination-scoped becauseassemblyis 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 intoprojection(everything populating the field list) andjoinedStep(thestepdeclaration plus what is now the only join chain), because the page request must observe the populated list beforedsl.select(...)is composed;assemblystays as the composition so its four existing call sites are untouched. Both paginating callers ride that seam, andBatchedRowsFragments.windowedPageTailextracts the plain batched child’s per-key windowing protocol (ROW_NUMBER partitioned by__idx__, pre-rank seek, the__rn__ <= limitouter 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; andjoinedDetailAliasDeclarations’s javadoc had been dangling since the conversion renamed its `{@link}target, invisible to the reference gate because private members sit below itsshowlevel. 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 aFilmContentSDL description rendering a false mechanism claim into the generated schema. The phrase graduated intoRetiredVocabularyGuardTestas a secondPHRASE_REGISTRYbeside the identifier one, matching the adjacencycross-table … joinrather 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.sqlas a habitat, which failed the build immediately on an unrelated pre-existing hit. Coverage: the capture-tier lowering pin where the term is decided, twoRootLauncherSqlBaselineTestcases (the page query and itstotalCounttwin counting the base under the discriminator restriction), aBatchedChildSqlBaselineTestwindowed 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 puttingDeliveryFactPinTeston gate duty. Retired: both deferral messages, the(DiscriminatedTable, Connection)constructor backstop half and itsRootLauncherRendererthrow,TABLE_INTERFACE_ROOT_CONNECTION_DEFERRED,crossTableJoinChain,crossTableAliasDeclarationsas a join concept,CrossTableField.aliasVarName(), aJoinStepjavadoc paragraph, andtargetAtMostOnePerSource, which never landed. Independent-session In Review → Done review, third cycle: full reactor green undermvn install -Plocal-db(5517 tests, 0 failures), zero code-string body assertions added across all seven commits, user-facing-doc check clean over fivedocs/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+7fcfb86the ten slices,dde4034the self-review sweep,d99b7f3the delivery record; Backlog → Specbd489b2, Spec revisions663adb2+6317a11+5449920, Spec reviewfb2d280):graphitron-mcpanswers 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’sWorkspace; ten tools, one resource and one prompt now read relations, and the module compiles againstgraphitron-modelplusorg.jooq:jooqand nothing else in the reactor. Four goal properties, each asserted inStoreClientBoundaryTest: one reactor compile edge, no reference to the language server in either tree, no read of theFieldClassification/TypeClassification/TypeBackingShapetaxonomies, no read of thewalk_family, and no connection the module opens for itself (DevQueryExecutorexcluded by name, its connections being to the consumer’s own database). Six artifacts left a published module’s compile and runtime classpath,graphitron-lspandgraphitronamong them, and the reactor now buildsgraphitron-mcpbeforegraphitron-lsp. The surface shrank before it migrated:edgeswas dropped rather than ported (its forward half is whatschemaanswers, and carrying the costume for the reverse half meant porting five labels, a six-permit node model and a memoised map inversion), andservices/conditions/recordscollapsed into onecodetool with akindselector, the three having shared an argument schema and a census and differed by aWHEREclause each.CatalogFactsdeleted 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 beingschema’s `kindvocabulary 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 todocs/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 undermvn install -Plocal-db(14 modules,graphitron-mcp145 tests, 0 failures), no code-string assertions on generated method bodies, user-facing-doc check clean over threedocs/files with the manual’s tool table build-enforced against the registered surface byServerInstructionsTest, 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 coversno.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 beinglsp-structural-consolidation.md’s slice 5, whose torn-read argument rested on `graphitron-mcpbeing a second concurrentWorkspacereader. -
R712 (
84e19b4; Backlog → Spec850c5bf, Spec revisions76c8103+e8793e1+5b609c8+cc1fedd+e6b58c2, Spec → Readyc0abe6f, Ready → In Progress3fd8048, In Reviewf108cd0): 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 newfact-model.adocsection 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 thirteenmeta_familyrows plus the prefix-lessdiagnosticplacement 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_errorstratum one on an unparseable input,graphql_schema_errorsplit along its ownstagecolumn (REGISTRYone,ASSEMBLYtwo, every assembly check being a predicate over captured rows),lint_stratum two althoughlint_findingis a table today, andbuild_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_synthesisand itsMacro synthesis provenanceheader, threeintent_view comments reading its text column), both explanation pages,MacroCapture,SdlFactCaptureand the two capture tests;effectiveFieldType→expandedFieldTypeis the only identifier renamed,authored_type_sdland theintent_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 theDerived stratum: claimsblock comment, theSemantic stratumsection header and the one table comment pointing at it by name (repointed atgraphitron_undecoded_argumenton the overflow property the two comments already share, not on a decode thegraphql_side does not perform), and thegraphitron_andintent_roster charters. TheDiagnostics stratumheader is left whole under the numbering rule, the preferred of the two options offered, and the twopipeline-overview.adocsites 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 undermvn 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.adocfiles, 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: theintent_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+274e2e9the reachable slices,5cdbaa6+758821d+4085989+6b201a2the substrate views,f6eba9b+0d4e2d1the doctrine repointing; the item ran from 2026-08-12 across many sessions and its earlier slices predate this clone’s history;21e1555restructured the body for the gate,33fe9f0+17d376crecorded the runtime edge and the seam figures, In Reviewdf5f307): 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 wasgraphitron-lsp’s pom, which named `graphitronand imported twenty-one generator types: it namesgraphitron-modelnow, thegraphitronedge 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 tographitron-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 bundleddirectives.graphqlsinto a graphql-java registry is gone,DirectiveSurfacereads 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@referencegets. The vocabulary became per-graph state reloaded when capture swaps, and the old constructor-time drift guard becameDriftDetectionTest, 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 byDiagnosticsStatementCountTestincluding that the count does not track the document’s size. In place of the declined shadow-parity gate,TriggerDispatchMatrixTestpartitions 13 sealedTriggerleaves x 6LspSurfacearms into answered / declared-no-answer / unimplemented, drawing its universe fromgetPermittedSubclasses()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.buildSnapshotran 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, takingFieldClassification,TypeClassification,DirectiveShape,InputValueShape,LspSchemaSnapshotand the@ProjectionForcoverage apparatus with it. The line counts moved in both directions and the item said in advance that saying so was the point:graphitron-lspmain 9,119 → 14,203 with thefactspackage (3,231 lines of queries and their row types) being the seam itself, againstrewrite/catalog4,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_nodeand 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 undermvn install -Plocal-db(14 modules,graphitron-lsp606 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_typeDDL comment namingCatalogBuilder.projectTypeDefinitionLocationsin 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’sdepends-oncleared, 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.mdbeing the strongest case for Discarded, plusmcp-snapshot-axis-key-naming.mdandlsp-compile-diagnostics-publish.md), eleven more needing a line edited, and the 29-type test-scope edge thatfact-store-test-harness-consolidation.mdowns. -
R711 (
b503a79; Backlog → Spec and the spec review84fe769, Spec → Ready0d69e4d, Ready → In Progress06b343b, In Review650b771): 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.expandFederationKeysaskedNodeDeclaration.isNodeTypebefore synthesizing a federation@key, and that predicate conjoins the SDL claim (@node, or@tableplusimplements Node) with__NODE_TYPE_ID/__NODE_KEY_COLUMNSread by reflection off the backing jOOQ class, so editing a generated class made an unchanged.graphqlsfile write differentgraphql_andgraphitron_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_typeis the inferred population, an@tablebinding throughintent_bound_tableatcandidates = 1plus animplements Nodeplus asql_node_metadatarow on the resolved table with nointent_node_metadata_defectrow 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@tablearm alone rather than on theintent_resolved_type_bindingreduction 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_typeunions that withgraphitron_node, theUNIONdedupe dissolving the predicate’s declared-wins short-circuit along with the provenance column that would have asked for it.intent_synthesized_federation_keyis federation’s node-entity rule, agraphitron_link.urlprefix match, membership, and no authored key whose decode is exactly the single pathid, which transcribes the live rule’s deliberate asymmetry where a malformedfields:argument decodes to no field rows and therefore reaches its detection instead of suppressing synthesis on a parse failure.intent_federation_keycomposes authored with synthesized at the authored grain,UNION ALLwith aNULLordinal 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 besideintent_resolved_node_key_column’s `JOOQ_METADATAtier is accepted rather than extracted, each view comment naming the other so the duplication is stated rather than latent. Capture surgery:MacroCapturehandles@asConnectionalone andexpand()takes no arguments,SdlFactCapture.baseSitesdies with the ordinal-takingcaptureTypeDirectiveoverload whileordinalsByTypekeeps its own unrelated reason, andNodeDeclarationleaves the capture API outright, offFactCapture.run,runWithDetectionsand everycaptureoverload, leavingjooqas the only catalog-shaped input with a javadoc saying nothing may add a second.graphitron_type_directive_synthesisretires, itsCHECKhaving closed its vocabulary to the one macro that moved, andgraphitron_type_declaration_synthesis’s `CHECKdropsFEDERATIONin the same sweep.ReachabilityRows.seedreplaces itsgraphitron_nodearm and its over-approximating@table-plus-implementsarm with oneintent_node_typearm, retiring the stopgap its javadoc named and narrowingintent_type_domainby 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.CaptureCorpusIsolationTestcaptures one registry twice, once with the catalog and once without, and requires everygraphql_andgraphitron_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) andFederationKeyTest(16) pin both views row-in-verdict-out in the module whose DDL declares them,NodeTypeShadowTestbindsintent_node_typetoNodeDeclaration.isNodeTypeat the membership grain because theintent_type_domainshadow cannot (a closure is not injective on its seeds),FederationLinkPrefixPinTestholds the view’s SQL literal toFederationSpec.SPEC_PREFIXsince a view cannot bind a query parameter, andFederationKeyDerivationTesttakes the six re-aimed capture cases out ofMacroCaptureTest, whose javadoc the same commit rewrote to say the class handles@asConnectionalone.CapturedStore.ofPipelinenow 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.adocgains 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@keyhaving always come fromKeyNodeSynthesiser’s registry rewrite. Retired: `expandFederationKeys,MACRO_FEDERATION_KEY,graphitron_type_directive_synthesis. Independent-session In Review → Done review: full reactor green undermvn 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.adocfiles (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 deadNodeDeclarationimports and, more consequentially, a{@link}inWarmStartRefreshTestnaming acaptureoverload whose parameter list still endsNodeDeclaration), a dangling reference that the reactor’s javadoc reference gate is built to reject but structurally cannot see, thejavadocgoal reading main sources only. Two improvements noted for whoever wants them, neither a gap the contract left: the isolation gate’s non-vacuity control assertssql_node_metadatais non-empty overall rather than forfilm_actorspecifically, which is the table the differential actually turns on, and the prefix pin asserts containment, so aSPEC_PREFIXshortened to a substring of the view’s literal would still pass. -
R680 (
4d64a26graphitron-model gains test sources and the M0/M1 harness,42b1477three tests go home and the two seeding prototypes reconcile,f1f2238the capture handle becomes graphitron’s shared harness,6ab3c4athe build level,22e680cthe guard,b481d60+eae6407+a423457+f6e9c34four seeded movers,6b18977+68b4255+c2e84d5+abdf559+274e2e9+2120b2e+c8e9dc9seven anchors split along the subject,05de542the facts writers get a level,8a8ec35graphitron-mcp’s two fixtures become layers,061dd07graphitron-lsp’s fixture becomes a layer and the guard’s pending list empties; to Speca6ec9a9, twenty-two spec revisions and two Spec → Ready sign-offs513b3e3+8045e08, Ready → In Progress54ceab7, In Review46bf7c2, rewritten as a delivery recordcb54c55): 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 isgraphitron-model’s question and gets seeded, what a `graphitronwriter or crawler puts in the store isgraphitron’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()andfileBacked(home)named rather than flagged) andSeededStore(named row-inserting helpers over the generated model tables) in a newgraphitron-modeltest-jar,FactWriters(the four shipped facts writers over a store somebody else opened),CapturedStore(a realFactCapturerun, layered closure form over handle over primitives, named arms rather than flags) andBuiltStore(a realbuildOutput()into a store on disk) ingraphitron’s, with `graphitron-lsp’s and `graphitron-mcp’s `StoreFixtureandStoreBackedBuildsurviving under their own names as thin local layers. Every level is usable without the one above it, andgraphitron’s two stand on the model’s; a single reactor-wide fixtures module cannot exist at all, since it would have to depend on `graphitronwhilegraphitron’s own tests depend on it. `graphitron-modelwent from no test sources to 26 test classes and a published test-jar, all structurally free ofgraphitron, and joinedGuardScope.IN_SCOPE_MODULESso its sources meet the citation and vocabulary rules from their first commit.StoreFixtureGuardTestis the one new test: one recogniser (a test-source reference toGraphitronModelStoreoutside 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:StoreReaderTestmoved 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 undermvn install -Plocal-db(14 modules,BUILD SUCCESS, 329 tests in the newgraphitron-modelsuite 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 agraphitrontype, 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-lspmigration editing none. User-facing-doc check clean over both.adocfiles (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 `SchemaReferencePagesTeststill opens a store directly, in the one moduleGuardScopedeliberately does not walk, so the guard’s promise is a module short of the tree and the spec’s claim thatroadmap-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 (
6870aabthe column-match sweep reads its claim view once,b3e2f7fthe javadoc gate stops forking,9ea52f8class-level test parallelism ingraphitron,3f85cffthe H2 materialized-view ruling gets a permanent home; filed86eed92, Backlog → Spec641f2d4, spec review1e612db+000da7e+a6d95a1+53a57b3+476c77c, Spec → Readydafca84, Ready → In Progress90db5b6, In Review5e9ec17): 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_claimcollapses its matches with a window overintent_field_column_scope, and a window sees its whole partition whatever predicate the reader applies outside it, soColumnMatchShadowTestreading it once per graph over a 55-graph store paid all 55 graphs' rows 55 times, quadratic in corpus size. The query now projectsgraph_name, runs once and pairs per example throughfetchGroups; the rows are identical because both anti-joins already correlated ongraph_namethemselves, 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 thecheck-link-referencesgate bound thejavadocgoal, whoseexecutePhase=generate-sourcesforked a lifecycle that had already run atverify; Maven runs that fork before the mojo body evaluates its skip parameter, sographitron-sakila-examplepaid for five duplicatedgraphitron:generateexecutions and then skipped the goal it forked for.javadoc-no-forkis the same mojo without the fork. Andgraphitronbecame the reactor’s first module to run test classes concurrently (methods staysame_thread, since a class owns its@TempDirand H2 store while two methods of one class share theirs), with@Isolatedon the two classes that rebindClassificationTrace’s process-global writer. On one 4 vCPU sandbox measured at both ends, `mvn install -Plocal-dbwent 9m06s to 6m44s and-T 1C8m26s to 5m58s, with thegraphitronmodule 181.2s to 70.6s. The fourth deliverable exists because the item file does not survive Done: the ruling thatCREATE MATERIALIZED VIEWis 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 (
79c81bbthe implementation,c822de0the README regeneration; filed304d00a, Backlog → Spec0fac740, Spec → Ready7073bd6, Ready → In Progressca4384d, In Reviewdb9574c): every built-in scalar the emitted schema references is registered on the builder, independent of what the author wrote. A schema declaringapplikasjoner: [Applikasjon] @asConnectionand namingIntnowhere generated without complaint and then failed at consumer startup withtype 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 itsadditionalTypeset 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 newTypeBuilder.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 inclassifyScalarType, the single producer the spec’s provenance finding asked for, over the assembled schema’s instance where there is one and over thegraphql.Scalarsconstant for a spec built-in the assembled schema omits. What makes the sweep total is a type-system constraint rather than a comment:registerSynthesisedtakes<T extends GraphitronType & CarriesObjectForm>, and it is the promoter’s onlytypeRegistry.registercall, 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-PageInfocorner falls out of the same routing, both arms ofregisterPageInfonow 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 anUnclassifiedTypedemotion suppressed so the author-caused case keeps its richer report. Because the guard sweeps the rebuilt assembled schema, it also covers the rewrite-timefirst: Int/after: Stringarguments 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 undermvn 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 nodocs/file; retirement sweep skipped by rule, the item declaring noRetired vocabularysection. 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 throughAppliedDirectiveEmitter.emitInputType, which embeds thegraphql.Scalarsconstant inline for a spec built-in, atypeReffor a federation-namespace name, and aGraphQLStringplaceholder for anything else, so none of them demand anadditionalTypeand sweeping them could only produce false positives;_FieldSetis sidestepped symmetrically, the guard and the generator’s registration filter both skipping_-prefixed names. And the spec’s finding 3 was wrong thatStringandBooleansat behind the same hole asInt: graphql-java always adds@deprecated(reason: String)and@skip(if: Boolean!),SchemaReachability.seedshas seeded survivor directive argument types since 2026-07-24, and no always-added built-in directive carries anIntargument, soIntwas the only scalar ever unregistered. The corrected inline comment onscalarRegistration_unreferencedSpecBuiltInIsNotEmittedencodes that, and the pipeline pin says in its own comment that itsString/Booleanassertions state the property rather than a behaviour change, which is the honest reading. The unspeccedCarriesObjectFormcapability marker is judged earned rather than scope creep: it is the mechanism the intersection bound needs, it follows theEmitsPerTypeFileprecedent of an orthogonal marker outside the sealed hierarchy, and collapsing the object generator’s six-leafinstanceofchain 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_SITES72 → 69). Its widening to includeNestingTypeis behaviour-preserving inresolveSynthesisedConnectionTypes, whose input isabsentMinted(), and aNestingTypeis 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, againstworkflow.adoc’s "implementer writes code, updates the plan" step: the In Review flip changed `status:andlast-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 retiredObjectTypeGenerator.graphqlTypeForwithout declaring it, leavingrelevance-ranked-search.mdinstructing its future implementer twice to add an arm to a deleted private method, repointed atCarriesObjectFormin the approval commit. -
R706 (
77c06c3the delivery, which also flipped In Review; spec landed inside9d9dc46, Spec → Readybd73a70, Ready → In Progressb1b40e0): a build that starts whilemvn graphitron:devholds 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=TRUEis 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 undermvn 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.adocfiles; 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 (
227a199the gate deletion,f8b7dabassembly as the gatherer’s own stage and the rooted traversal,bfa41abthe anchor tables,7c6d938the rework pass; In Review8403b04, body reconciledb892ae1, first Done gate → Readyd4eb242, In Review againa23af39): the SDL fact gatherer is a five-stage pipeline and thewalk_membership gate is gone.intent_authored_claim_conflictis total over the authored claims with each consumer applying its own population join (the build-error surface joinsintent_type_domain, the editor reads the view ungated), sowalk_claim_domain_type,walk_claim_domain_fieldandClaimDomainRowsare deleted andWalkReachdissolves into the typedClassifiedRundiscriminator. Assembly is the gatherer’s own stage and itsASSEMBLYverdict 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) writingintent_type_domainfrom SDL-only seeds scanned over every implementing type, replacingReachabilityRows’ SQL closure; the node seed is `implements Nodealone, 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 thegraphql_*_coordinateanchor family rather than the filed composed-census transcription: existence and attributes split per relation, the whole FK web re-pointed onto the anchors,SdlCoordinatesowning every coordinate’s first-wins claim, and capture’s merge pinned against graphql-java’s composition bySdlCoordinateCensusTestat 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 undermvn install -Plocal-db; the sweep clean, the two survivingwalk_claim_domainmentions 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 (
dbc619athe delivery; filed and Backlog → Specc02d901, spec revisionsa41084a+9dae6ee+95ae64e, Spec → Ready7f2ff35, Ready → In Progress31a1844, In Reviewa76d0e2): 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 canSELECTit) 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, aCHECKrefusing the length-one cycle declaratively, and the family’s first machine-written resident, its charter sentence saying so.MaterializeDependencies.populateis its one writer: for each registration it takes the source view’s storedINFORMATION_SCHEMA.VIEWSdefinition, 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.refreshOrderreturns its ownRefreshOrdertype rather than a secondList, Kahn over the rows with an alphabetical tie-break so a row-free relation yields exactly the census order byte for byte, consumed by bothrefreshandrefreshAll, refusing a cycle defensively with the cycle named. The build-time acyclicity gate inMaterializeRegistryGateTestis the invariant’s home and replacestheRegistryNeedsNoOrderingYetalong with the word-boundary regexclosureOfit 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 onFactCapture’s stratum comment rather than enumerated by hand. DDL also gains `UNIQUEonmeta_materialize.target_table_nameon its own merits, andfact-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 undermvn install -Plocal-db(BUILD SUCCESS, zero failures,MaterializationOrderTest8/8 andMaterializeRegistryGateTest6/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, itsrefreshAllsibling, and the named-cycle case) and so does the pipeline gate’s new fixture registration on real store machinery, failing exactly asintent_fixture_bindingunpopulated. The spec’s "changes no timing whatsoever" claim survives the added boot-time parse:GeneratorDeterminismTestmeasured 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:deriveDependenciesruns afterstampinopenAt, 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; andtheRefreshOrderRespectsEveryDependencyRowis 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 (
608f047the delivery,83d45b1In Review; filed insideabaa666, Backlog → Spec1e26a84, Spec → Readycce3426):intent_class_assignableis deleted rather than repaired. The all-pairs transitive closure overjvm_class_supertypehad 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-attachingsource_nameby 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 scaleClassAssignableTestwas 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_pairre-citingintent_class_member_slot,intent_type_backing_classstating its own no-route-column rule,intent_delivery_containerspeaking of a closure rather than the closure, andjvm_class_supertyperecording 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_bearingrecursion carried the same defect under the sameUNION ALLand is deduplicated in the same commit, recursing over aninput_object_field_edgeCTE, consumption being throughEXISTSso 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 undermvnd install -Plocal-db(BUILD SUCCESS, all four roadmap-tool gates clean,check-schema-identifiersresolving 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 inClasspathScanner,ClasspathScannerTestandCompletionDatareading as the posturejvm_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.everyRelationIsRegisteredgates both directions, and the addedaDeepInputChainWhoseLevelsRepeatTheirFieldTypeStillAnswerswas re-measured with the dedupe reverted in a scratch patch, givingTimeoutExceptionat 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 (
452c497the delivery,821890cIn Review with the re-measurement; filed and Backlog → Spec969803b, spec review2d16bf5, Spec → Readyd972d9e, Ready → In Progressa1b17e9; Done-gate reviewd865b68requesting 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.fileandjavac_diagnostic.filenow hold the path their writer read, thediagnosticview derivesfileanddirectoryfrom paths in all seven arms, and theCREATE ALIAScarrying inline Java source that H2 compiled with javac on every schema execution is deleted, which makesModelCodegenDriver’s "the schema declares no routines" comment true. The two boundaries whose protocol names a document by URI convert at their own edge instead: `DiagnosticFactsdecodes a document URI to a stored path once and both replay entry points go through that decode,LintFixescompares the source name it already holds, and MCP’sDiagnosticFacets.Spellinggains an outboundrenderbesidenormaliseso the group key, thefileslist and each entry’slocation.uripublish a URI over a stored path.SourceUriis the one home for both directions, gaining the null guard its javadoc promised andofDirectory, which renders a directory as the URI of a file inside it with that segment removed:Path.toUriasks the filesystem about a trailing slash, so converting a directory path would put that directory’s existence on the wire.ValidationReport.sourceUrisretires withaddCanonicalandcanonicalUri, 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 byDiagnosticFactsTest.noFileColumnSpellsAUri, a property that can fail, asserting no value in the fourfilecolumns and none the view projects ontofileordirectorybegins withfile:, 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;SourceUriTestpins 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 pergraphitron: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 (
210bcb9the delivery; filed inside63db590, Backlog → Specdf65e10): two argument values escape the closed-vocabulary bet the diagnostics aggregate rests on, and now neither silently answers a different question.orderByread asstringArg(...).map("key"::equals).orElse(false), so every value that was not exactlykeymeant count with nothing in the response saying the argument was ignored; a nestedOrdering(count,key) now resolves through anofinDimension.of’s shape, refusing an unknown value with both named, read off the argument map rather than through `McpWire.stringArgso a blank or non-string value is a refusal too, and the aggregate’s input schema declares theenumfromOrdering.wireNames()so the pair is discoverable without a failed call. AndDiagnosticsTool’s `severitysugar lowercased its argument where the sharedwherepath did not, soseverity: "ERROR"filtered andwhere: {severity: "ERROR"}matched nothing; aSpellingenum is now a fourthDimensionconstructor argument on the four columns holding a case-folded store taxonomy (severityandsourcelower-case,kindandattemptKindupper-case with hyphens folded to underscores), applied incoerce, the one boundary both diagnostics tools share, so the sugar dropped its owntoLowerCaseand agrees withwhereby construction.matchesStoredis 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 storedINVALID_SCHEMAasrejectionKind: "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:orderBydeclares anenumto be no more lenient than,severitydeliberately declares none, the store owning which severities exist. Five pins inDiagnosticsAggregateTest, two of them discriminating rather than regression-only. Independent-session In Review → Done review: full reactor green undermvn 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.adocand 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: thediagnosticview confirms every declared spelling (severitythe'error'/'warning'literals with compile rows projected throughCASE WHEN j.kind = 'ERROR',sourcethe'schema'/'compile'pair,kindenum names plus two literals,attemptKindRejection.AttemptKind.name()), and the four dimensions leftAS_STOREDthat 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.entrystill rendersrejectionKindkebab-case by hand whileSpelling.UPPER_SNAKE.renderis the identity, so the module publishes two spellings of one taxonomy value and only the inbound fold hides it (a coherence wrinkle R759’s laterrendergeneralisation created, not this item); and theseveritysugar still reads throughMcpWire.stringArg, soseverity: 5orseverity: ""drops the filter and answers over every severity, the same silent-different-question shape this item closed fororderBy, left standing because the summary line at least stops claiming the filter. Both are Backlog material, neither is scope the spec named. -
R771 (
d27f54bthe delivery,e233b1bIn Review; filed49c2253, Backlog → Spec4699fdc, Spec → Ready99962e4): 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 `MULTISETon its own key or pair it as a second statement, withSchemaQueriesnamed 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.mdis the procedure over it: six steps from naming the grain to closing with the pin, per-surface guidance splitting hand-written store reads (imitateSchemaQueries, verify overSeededStore) from@conditionfixtures (the alias-bearingtable.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 fromandthe driving relationfor the FROM-clause origin and neveranchor, which the store’s DDL andLaunchSource.AnchorTablealready 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, neverfile:line, no restated measurement) is the mitigation. Independent-session In Review → Done review: full reactor green undermvnd install -Plocal-db(BUILD SUCCESS, 14 modules,check-schema-identifiers,check-adoc-tables,check-transient-citations,check-module-enumerationandcheck-adoc-xrefsall 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.adocpages, the eight class names,ParentCorrelation.parentKeyColumnsandLaunchSource.AnchorTable,SeededStore.derive,ReferencePathConditionFixtures.splitFilterParentIncludedand its concrete parameter types,intent_column_match_claim’s comment carrying the measurement the skill declines to copy, `FactSchemaGateTest.commentCoverageIsTotalasserting presence over relations and columns and nothing more as the skill states, andSchemaQueries’ own javadoc confirming the two-statements-paired-on-the-type’s-own-key shape the skill sends readers to imitate. The `descriptionfront-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 `ca3d10cthat 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 agraphitron-modelderivation 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 onSeededStore. -
R773 (
d52d0b1fthe mechanism and every consumer’s posture; Backlog → Spec43e7477c, spec review findings0e1cfe58, spec revisionf0016510, Spec → Ready12f8e450, Ready → In Progressf7986301): every fact-store read now runs under aReadBudgetits 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, andStoreReader.readserializes 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-modelgainedReadBudget(Bounded/Unbounded, rendering its ownSET QUERY_TIMEOUTsession command) andStoreAnswer(Answered/OutOfBudget, carrying the statement and the budget it overran), with the predicate keyed on H2’s vendor code 57014 rather than onSQLTimeoutException, because a lock timeout is the same type with the opposite remedy;GraphitronModelStore.reader()is replaced byreader(ReadBudget)with no defaulted overload, andFactCapture.timedOutOnALockgained 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 threeStoreAccessdoors partitioning the three read grains exactly as they already stood, soansweringstopped delegating toansweringAlland 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 (
d34fabfthe delivery,85b00caIn Review; filed inside R773’s spec review0e1cfe5, Backlog → Specdb5c358, bounce-count correction66197ce, precedent survey1e98de1, spec review findings44fbc24, author revision3d3ed43, Spec → Readya8de6d3): 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.adocsplits the singleSpec --> Specdiagram 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. Thesrpskill’s Spec-stage Verdict bullet hands the reviewer a## Reviewer findingssection 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. Theroadmapskill’s transition table splits itsSpec | Specrow to match the diagram. The reviewer-rule guard is untouched,Main.ALLOWED_TRANSITIONSstoring targets as aSetand 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 inroadmap/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’sdepends-onon 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 undermvnd install -Plocal-db(BUILD SUCCESS, 14 modules, 6143 tests, 0 failures, withverify-roadmap-readme,check-adoc-tables,check-transient-citations,check-schema-identifiers,check-module-enumerationandcheck-adoc-xrefsall 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 nodocs/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 `2f51ca6that 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 omittingroadmap/workflow.adocwhere the Implementation-stage template includes it, which is scope the spec did not name and is Backlog material. -
R772 (
b3d61b7the delivery,e0bfffdIn Review, post-Done follow-upse920581+356c548; filed and Backlog → Spec06f0abe, spec revisions816d8ff+2b57543+f519dce+afaf1d7+3827c60, spec review findingsd66ff23, Spec → Ready0981559, Ready → In Progresscae9186): the dev loop’s live fact store has a door. Agraphitron:devsession 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 wholepsqlcommand; 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 createdMODE=PostgreSQL, holding oneREADONLYlinked table per store relation with H2’s PostgreSQL server in front of it, andGraphitronModelStore.console(int)mints it besidereader()so the URL never leaves the class. Three constraints were measured rather than reasoned about and each is recorded inStoreConsole’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 withSchema "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 includingSET extra_float_digits = 2which H2 rejects as a syntax error with noassumeMinServerVersion/preferQueryMode/optionscombination getting past it, which is why the surface is a command line rather than a JDBC URL and why the protocol pin shells out topsql; and psql’s backslash commands do not work against H2’s partialpg_catalog, so introspection goes throughinformation_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 on127.0.0.1and 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, andh2.bindAddressdoes confine the bind but H2 reads it once when its first class initialises, so the goal setting it at the top ofexecute()is necessary and not sufficient in a JVM whereModelCodegenDriveralready opened a store.StoreConsole.opentherefore 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 helperclose()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 mistypedUPDATE; 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 fromStoreConsole.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 newstore.consoleMCP tool, advertised on every boot incatalog.search’s shape so the disabled arm can hand back the enabling command, deliberately not folded into `status, itsconnectCommandthe console’s own string fromGraphitronMcpServer.ENABLE_STORE_CONSOLEandStoreConsole.coordinates()so the human’s line and the agent’s cannot become two spellings. Independent-session In Review → Done review: full reactor green undermvn install -Plocal-db, run twice, the second time after a rebase that brought R769’s per-threadgraphitron-modelstore fixture onto the sameFactStoresfile this item extended (BUILD SUCCESSboth times, 14 modules, 6171 tests, withverify-roadmap-readme,check-adoc-tables,check-transient-citations,check-module-enumeration,check-schema-identifiersover 219 relations andcheck-adoc-xrefsover 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.adocfiles; 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:StoreConsoleTestran 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, andStoreConsolePsqlTestran 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 tenStoreConsoleTestclaims the spec enumerated are present, including the forced-verification-failure arm driven through aBindCheckseam, 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-wideh2.bindAddresssurefire property in the parent pom is necessary (the property has to arrive with the JVM and nographitron-modelfork can set it in time, test ordering deciding which fork loaded H2 first) and costs nothing else, verified by disassembling H2 2.4.240’sNetUtils, wheregetBindAddressis reached only fromcreateServerSocketTryandgetLocalAddressso client sockets are untouched, and by confirmingStoreConsoleholds the reactor’s onlycreate*Servercall; and the explicit console teardown on the MCP-bind-failure path out ofexecute()closes a real leak the spec’scleanup()placement did not cover, pinned by a case that drives the whole ofexecute()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.coordinatesOfwith 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 ofexecute()aftersessionStoreopened and before the shutdown hook is installed, leaving that store unclosed on the one path, which is the same shape as the pre-existingresolveDevDatabasedialect stop and matters only under a survivingmvnddaemon, 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.e920581puts-Xin the printed command: psql runs the developer’s~/.psqlrcbefore anything else, and a startup file written for a real PostgreSQL server sets things this server does not have (application_name,bytea_outputand 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-Xto 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 ownHOMEandPSQLRCand sends stderr to a file rather than a second pipe that could deadlock against the read of stdout.356c548is javadoc only, recording onFactStores.perClasswhy 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,withSeededStorehanding a body aDSLContexton 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 undermvn install -Plocal-dbon the settled tree,StoreConsolePsqlTestruns 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-Xreached the log line and the MCPconnectCommandfield together and the two still cannot diverge, and the new case was proven to bite by removing the-Xand watching it fail on exactly the documented H2 syntax error rather than on faith. -
R785 (
17d8154the fork,646bef5the guard,5e39131In Review; filed6f425dd, Backlog → Spec7b66503): 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 bySystem.loadon a fixed cached path,exec:javaran it inside the Maven JVM behind a class loaderexec-maven-pluginrebuilds 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 withUnsatisfiedLinkError: 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.mdrecommendingmvndwhose 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: thebuild-docs-indexexecution bindsexec:execwith${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 FFMSymbolLookup; 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-UNNAMEDis now declared where it applies, the replaced pom comment having claimed surefire’sargLinecovered this execution when thatargLineconfigures test forks and this ran in the Maven JVM; and the generator’smainis 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 ascheck-native-load-isolationatverifybesidecheck-coverage-agent-wiringand 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 startinglangchain4j-embeddingsoronnxruntimemust not bind anexec-maven-pluginexecution on the in-processjavagoal. 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 1Cbecause every CI job is a fresh JVM. Independent-session In Review → Done review: full reactor green undermvnd install -Plocal-db(BUILD SUCCESS, 14 modules, no failing suites, 634 chunks at dimension 384 over 79 pages throughexec:3.5.0:execin phase,check-native-load-isolationandcheck-coverage-agent-wiringboth reporting clean,NativeLoadIsolationCheckTest14 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 nodocs/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 toexec:javain that same daemon the first embed succeeded and the second died with the verbatimUnsatisfiedLinkErrornaming/root/.djl.ai/tokenizers/0.21.0-0.36.0-cpu-linux-x86_64/libtokenizers.so, andrun_againstThisRepository_isCleanerrored 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 ongraphitron-mcp) would not be marked, and the dependency quarantine confininglangchain4j-embeddingsto 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 (
0120ad9the delivery,b4e8f00the rebase note; spec review81c190e, author revision9c7bd93, Spec → Ready04ef40c, Ready → In Progress557a231; 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.withSeededStorenow delegates to a package-privateThreadConfinedStorebeside it: aThreadLocalboots 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 isTRUNCATEover every base table outside themeta_registry family andstore_stamp, underSET REFERENTIAL_INTEGRITY FALSErestored in afinally, 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.populatereading the registry and the catalog’s stored view definitions and no fact relation, which is the settlement that makes this slice cheap;SeededStore.deriveis the other derivation, does depend on fact rows, and is still called per case. The partition is derived fromINFORMATION_SCHEMAonce 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:verifyBootStateasserts at boot that every table the clear empties was empty on a freshly booted store, andverifyClearedasserts 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 aninUseflag 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()againstbootingThreads()as an equality, which is the confinement invariant itself, and a total onFactStores.boots()under a ceiling. The equality is pinned rather than the literal four becausefixed.parallelism=4sizes aForkJoinPoolthat adds compensation threads when a task blocks, and this module’s own run boots eight stores on eight threads, soboots == 4would 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 inThreadConfinedStore.runon 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 becauseFactStoresis 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. Alsojunit-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 fivejunit.jupiter.*settings untouched; aHOMESentry inStoreFixtureGuardTestdeclaring the one fixture in the reactor that owns a store’s lifetime rather than handing it out; andRunawayRelation’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 nodocs/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: addingSTORE_GRAPHto 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 theMETA_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 plusstore_stamp.ThreadConfinedStoreTestran 7 of 7 with none skipped, so the equality pin and the budget assertion both actually executed, and the module ran green twice more underClassOrderer$Randomwith 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 atThreadConfinedStore,verifyClearedandBOOT_BUDGETin 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 (
ad5aad8the delivery,4e2583athe rework round; filed and Backlog → Speca0f3646, research notes479515c+1d81023+3437ef8+b732c78, Spec → Ready1e3c082, Ready → In Progress1202c45, In Review → Ready0e4ed68):graphitron-sakila-exampleruns 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:teston 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.QuarkusTestExtensionkeeps its per-test bookkeeping in static single slots, so two@QuarkusTestclasses in flight invoke one class’s method against the other’s instance, and parallel@QuarkusTesthas never been supported upstream; the four classes whose subject really is the wire now share one@ResourceLockkey carried byQuarkusTestLock, which makes them mutually exclusive while leaving them free to overlap the other sixty-odd, andQuarkusTestLockEnforcementTestfails the build when a new one arrives without it.TutorialSmokeTestwas the fifth and needed no container: every assertion it makes is on GraphQL response shape, so it moved to the directquerydbharness, and its unscopedDELETE FROM film WHERE film_id > 5became a delete of the one row the one writing case creates. The larger half was sixty-three assertion sites that only held whilefilmorcontentheld nothing but the seed, against eleven classes that writefilmand two that writecontentwhile 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.ratingcarriesDEFAULT 'G'so a G-filtered assertion counts other classes' rows and PG is the value the seed can own,film.lengthcarries 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'emailcleanup 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 variedrating,length,release_yearand 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 thefilmsOrderedConnectiontitle cluster survived to be caught at In Review by a single row titledA 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 (
02f1795the delivery,1dbdd38the rework round; filed and speccedaf45631, sync passesebee6c4+bb737f6+0e643f3, Spec → Readyf0b6ab0, Ready → In Progress6b4475c, In Review → Readyd21986c, rework In Progress1b864e1, In Reviewec69740): the diagnostics drain no longer runs on the thread that triggered it.Workspace.enqueueAndNotifyran 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. FordidOpenthat 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,$/cancelRequestincluded, 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 beforeroadmap/lsp-surface-latency-budgets.mdreproduced it in a harness at 31310 ms viadidOpenagainst a median of 4 to 20 ms for every other surface.markAllForRecalculationoccupied the dev loop’s watcher thread the same way. The fix is one injectedExecutoronGraphitronTextDocumentService, 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.DevServerandLaunchereach 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: anAtomicBooleancollapses 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.holdsViewFormakes the publish loop skip a URI the workspace no longer holds a view for, which closes the one hazard asynchrony genuinely introduces, adidCloselanding 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 afterdidOpenreturns passRunnable::runand 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 isroadmap/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 `finallyshuts the drain executor down per connection, nothing clearsWorkspace’s single listener slot, so between an editor detaching and the next connection’s `setClienta build swap still reached the dead connection’s service and the submit was rejected. Reproduced rather than reasoned,RejectedExecutionExceptionleavingenqueueAndNotifyonto a Maven thread, andDevMojomade it worse rather than absorbing it:regeneratelogged "catalog refresh after save failed; keeping previous" for a refresh that had in fact succeeded, then threw a second time out of an innercatch (RuntimeException)past an outer catch that only handlesMojoExecutionExceptioninto aDebounceExecutortask whoseScheduledFuturenobody inspects, andrebuildCatalogcalled 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’sRemoteEndpoint.notifycatching 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 failingrejectedSubmitIsAbsorbedAndDoesNotWedgeTheFlagon its own assertion, and round 1’s original reproduction against a genuinely shut-downThreadPoolExecutornow 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.notifyas "the mutation’s real cost is the diagnostic recalculation it triggers" when after this changenotifyis a flag-and-submit and a large one means the listener has stopped being one, and itsthread=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 withenqueueAndNotify’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 undermvnd install -Plocal-dbon the rebased tree,DiagnosticsDrainThreadingTest4 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:markAllForRecalculationstill callsloadVocabularyinline, so a build swap can still spend the session budget on the watcher thread, and because that call sits outside both themutateandnotifyspans the trace the rework just corrected attributes the time to no span at all. -
R792 (
b7f5026the delivery,4fa6c12In Review; filed and speccede5363d4, sync passesebee6c4+0e643f3, Spec → Ready7646664, Ready → In Progressaf4dafc): the language server’s out-of-budget warning names the read instead of dumping the statement.StoreAccess.warnedlogged 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 newStoreReadenum besideStoreAccessdeclares 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 ofStoreAccess’s doors plus the two `Workspacefacades in front of them now take one. R795 landed a fourth door,annotating, while this gate was open, and it takes aStoreReadlike 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, spellingno.sikt.graphitron.lsp.state.StoreAccessout 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.OutOfBudgetkeeps 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 undermvnd install -Plocal-db(BUILD SUCCESS, 14 modules,lsp.StoreOutOfBudgetTest5 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:theWarningNamesTheReadAndTheStatementDropsToDebugprovokes a real overrun throughRunawayRelationonstore_graph_sourceand captures the boundary’s own logger with a logbackListAppender, 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 andStoreAccessis the module’s only store read, so the vocabulary is complete for the tree it claims to cover. One non-blocking note: onlyDIAGNOSTICSis 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 (
480bb20the four steps,18cea5ethe door enforcer,4396dfcthe rework round,ea3f529the ceiling lesson; filed1d7f91c, Backlog → Spec83360d9, sync passesebee6c4+0e643f3, spec review2edae5f, spec revision39cb1d2, Spec → Ready347b798, Ready → In Progress19cef69, In Reviewdd575e7, In Review → Readyaabf6c8): 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 statementDeclarationFactsbuilds:redirectArmjoined the catalog censussql_tableto the derivedintent_type_backingon 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, anINsubquery 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, withCENSUS_ORDERrestoring 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 andInlayHintConfig.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 tightenINTERACTIVE_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 withLspTrace’s 100 ms slow-span tag, and what splits is the door: `StoreAccessgains a third reader and a fourth door,annotating, whichinlayHintalone 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 isSurfaceScanCountTest, a new instrument in the tree, reading thescanCountH2 reports underEXPLAIN ANALYZEover 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:DeclarationDefinitionStatementCountTestpins definition at one statement per request and that one statement is the bug. Ceilings are the broad net andtheCensusLookupDoesNotTrackTheSchemasSizeis 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.adocstill said the session mints three readers with two behindStoreAccess, enumerated the doors withoutannotating, 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 nodocs/file. The rework rewrote that paragraph to four readers with three behindStoreAccess,annotatingnamed 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 throughannotating, 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, andtheInlayRequestIsRoutedThroughTheAnnotationDoornow 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: withredirectArmreverted to the flat join bothSurfaceScanCountTestcases 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’smeta_materializeregistrations, since that change moves the counts the ceilings measure. Full reactor green undermvnd 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. OnlyDeclarationDefinitionsandDeclarationHoversreadDeclarationFacts, so four of the seven ceilings were never mutation-tested against this defect, and the sequential assertions mask each other besides. -
R805 (
712f272the delivery,a310024the three gates landed ahead of the move; filed1ba9567, fork resolved4f1e84c, Backlog → Spec80eb452, consult folded90f05a4, Spec → Ready5b92d57, Ready → In Progress27e98ed, In Review74ad4ae):docs/architecture/grows aprinciples/section beside the three Diataxis quadrants, anddevelopment-principles.adocmoves 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 inexplanation/with its reference web) and carries no axiom roster and no count, so a seventh axiom falsifies nothing;architecture/index.adocintroduces 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: theARCH_QUADRANTentry flips toprinciplesand theMain.javaemit sites follow, the anchored xrefs fromtyped-rejectionanddispatch-axesbecome cross-directory, the concept page’s authored href repoints, the prose paths inCLAUDE.md, thesrpandreviewer-promptskills, theprinciples-architectagent 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:AdocXrefAnchorCheckwidens to unanchoredxref:targets with the path verdict a function of source provenance (build failure for pages authored underdocs/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 indocs/pom.xml;ArchQuadrantBindingTestbinds 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;HowToIndexCoverageTestparameterizes 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 undermvnd 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-maintaineddocs/pom.xmlroster 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 (
267cde8the delivery; filed1ba9567, corrections taken back from R803’s revisiona4171d5, principles consult foldedc2a5bd3, Backlog → Spec64d356f, spec reviewea76bbf, spec revisionaf314db, Spec → Readyddd0cce, In Progress → In Review267cde8): the rule governing what a single fact column may hold is stated on the two explanation pages, so the next author reaching forLISTAGGmeets an argument rather than only a gate. R803 had already converted the serialized-collection columns to rows and landedCollectionValuedColumnGateTest; 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 aboutARRAY_AGG.intent_type_backing_conflictcarries both verdicts under one key:candidatespasses, the same set joined into one string would not, and the contesting classes are rows onintent_type_backingunder 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’sdiagnosticplacement exemption is scoped to that relation’s name and population, withcoordinatepassing 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.adocgains the failing sibling of theintent_bound_tablecandidate 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 undermvnd install -Plocal-db(BUILD SUCCESS, 14 modules, none skipped,CollectionValuedColumnGateTest2 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_backinganddiagnostic.coordinateare live and their own comments carry the arguments the paragraphs attribute to them,intent_authored_claim_conflictno longer carries the directives and its comment says why in the same terms, the gate exists ingraphitron-modeland 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, sinceAdocXrefAnchorCheckscans cross-filexref: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 (
c5d5da2the delivery; filed73242e1, Backlog → Specb455425, spec correction50cffd2, spec review round 12ae532a, spec revisionab33377, spec review round 27aea6bd, reviewer’s round-2 tweak46abd93, author’s draft-readingbcdcdd2, Spec → Ready4a576bf, Ready → In Progresse276f4d, In Progress → In Reviewf085c77; Done-gate review in this commit): ameta_materializeregistration 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-multiplicityranks breadth and says breadth is not cost,SurfaceScanCountTestholds ceilings over reader surfaces and none over a relation, andMaterializeRegistryGateTestasks whether a registration is well formed and not what it costs anybody.DerivedReadCostTestprices 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 fromMaterializeDependencies.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.UnregisteredRelationis the instrument,RunawayRelation’s rename-then-create sibling taking a `Registrationrather 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_decodegains the cost warning its own comment owed, andfact-model.adocgains 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-registeringintent_resolved_type_bindingdoes not make the decode family cheaper by anything worth naming, and the two large findings belong instead tointent_field_reference_step_hop, the registration this item was found alongside and had credited with recovering most of the decode’s gap, which costsintent_field_reference_step_target32x andintent_field_column_scope_live10x, 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 undermvnd install -Plocal-db(BUILD SUCCESS, 14 modules,DerivedReadCostTest4 of 4 in 50 s,UnregisteredRelationTest1 of 1,MaterializationOrderTest9 of 9). Completeness was checked rather than read: the ratchet was mutation-tested at the gate by deleting the binding pair fromKNOWN_NON_MONOTONIC, which failsaRegistrationDoesNotCostAnotherRelationMoreThanItSaveswith 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 throughRunawayRelationrather 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 onfact-model.adocwith 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 (
1491f9athe model DDL,60d01fethe capture registration,93d2ee8the execution-tier fix,8b5b23dthe roster, re-price and re-measurement,6c94642and940e05dthe two later re-pins,adf6cfcthe rework round; filed105a2cc, Backlog → Spec6212628, Spec → Ready3bb36a2, Ready → In Progresscceeae3, plan collapsed3e373f6, In Progress → In Review39d9583, In Review → Ready1f7684f, In Progress → In Review7d5811b; Done-gate review in this commit):intent_node_id_instructionis 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 theTARGET_TABLE_NODE_TYPEarm that joinsinstructedtoslot_tablederived from it, and H2 inlining a non-recursiveWITHexactly 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_liveview, so the item declares no retired vocabulary and means it. The fix had been stranded onquickfix/10.0.0-RC34since 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 drivingFROMand 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:CELLSfell 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 tointent_argument_scope_tablewent with it, and the reason matters more than the drop: none ofintent_node_id_encode,intent_node_id_decode_slotorintent_node_id_decode_defectnames 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_endpointnames 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_livehaving landed on trunk before this item started work; the two re-pins after it revisitedDerivedReadCostTestand 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_liveandintent_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 undermvnd 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 to8c56f05apart 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_livenamesintent_argument_scope_tablethree 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 vocabularysection 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 (
835013fthe decode-slot lift,dfaf5eethe per-column partition and the obligation-consuming emitters,ba40389fixture, schema, coverage and docs,f2bad1bIn Progress → In Review; filedefbe7f8, Backlog → Spec0b75a55, Spec → Ready3f1bfc3; Done-gate review in this commit): a cross-table@nodeIdreference 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, andSetColumn,KeyColumn,MapBindingandRecordBindingall carry a slot the readers emit asvalue<slot + 1>(). The fixture is built to catch exactly that:catalogue_item’s foreign key to `cataloguelifts(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.AgreementObligationis a component on theUpdateRowscarrier 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,buildCarrierUpdateChainSingleandbuildCarrierBulkPerRowUpdateBody, 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 widenedMixedCarrierKeyMembership: an explicit null would write NULL into the out-of-key half and leave the in-key half populated, whichMATCH SIMPLEaccepts as a satisfied foreign key, and "don’t straddle your own key" and "make this reference non-null" are different fixes for tooling switching onlspCode().MixedCarrierKeyMembershipsurvives, 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 fusedkeyGroupsOfdiscriminator would have moved arity-1 decoded key columns between theDSL.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; carryingdecodeSlotonMapBindinginstead removes the same defect at four readers rather than one, with every existing binding at slot 0 so the bytes are unchanged.render/LookupRows.slotValueExprneeded 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 undermvn install -Plocal-dbagainst a private repo head and a per-worktree database. Completeness was checked rather than read off the plan.DmlSqlBaselineTestpins the contract’s emitted statement literally, one SET column with each key column named once on the single-row arm and av(...)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;StraddlingReferenceUpdateExecutionTestproves the same-tenant re-point writes onlycatalog_codeand does not move the row, and that a cross-tenant input throws with nothing written on both arms;MutationDmlNodeIdClassificationTestasserts partition, slot and obligation on all four carriers, which is what makes a consumer unable to drop the component silently; and the untouchedSelfFkNodeIdUpdateExecutionTestandBulkUpdateSetAgreementExecutionTeststayed 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 skippedgraphitron-sakila-examplethe 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 (
f79d83eethe delivery and the amendment; filedeb3e6ffae, Backlog → Spec648df10a, Spec → Ready1f961b5a, Ready → In Progress5bd0cdee, In Progress → In Reviewe6c2aa6c; Done-gate review in this commit): whatintent_node_id_instructiondoes 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, givingintent_field_scope_tableitsPARTICIPANT_TABLEarm, 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 innode_type_nameand 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 writtentypeName:, 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 inNodeIdInstructionTestcover 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; andresolveNodeIdArgTargets’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-dbagainst a private repo head and a per-worktree database (BUILD SUCCESS,NodeIdInstructionTest25 of 25,MultiTableFilterLoweringTest16 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 nodocs/file; retirement sweep skipped, nothing retired; no code-string assertions on generated method bodies, the new pins being store rows and classification verdicts. -
R676 (
e79578484the decode routes and the escape,a748e1490tests, fixture, docs and the tightened refusals,92c565c19the argument coordinate’s lift test; filed822673552, Backlog → Speceb3e6ffae, Spec → Ready0434a855a, Ready → In Progress48bbbddf2, In Progress → In Review5d06eaaae, rework round420e4aa3c/fcde2393b, back to In Reviewce7fb5331; Done-gate review in this commit): a@nodeIdfilter input on a multitable query can state a per-participant join path, and@condition(override: true)genuinely takes the predicate where no route resolves.@referenceForwidened toINPUT_FIELD_DEFINITION | ARGUMENT_DEFINITIONunder 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 inNodeIdLeafResolver.resolveFkJoinPathbecame a sealed choice (uniform@reference, per-participant@referenceFor, auto-discovery),Resolvedgained theAuthorOwnedPredicatearm, and the three auto-discovery refusals name@referenceForand the override escape under a participant while single-table wording stays unchanged.lowerParticipantFiltersstops 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 aderive/join (ReferenceForParticipantDefects) rather than a stored view, the argument coordinate gains thegraphitron_argument_reference_forcapture family, and the docs land inreferenceFor.adoc, the global-id how-to,nodeId.adocandcondition.adoc. Four deviations disclosed in the spec body rather than silent: the participant ridesNodeIdArgPlanbesideClassifyContext, 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 referenceNodeIdEncoderbecause@conditionclasses 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 undermvn install -Plocal-dbagainst 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 wherefilmreacheslanguageonly ambiguously andinventoryonly throughfilm, 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.NodeIdOverrideConditionFkTargetPipelineTestand the single-tableGraphitronSchemaBuilderTestrejection 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.