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

Rewrite Changelog

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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