Rewrite Changelog
-
R465 (
a9deae5, Specc3ae081/145afd1): Bump the two graphql-java satellite artifacts in the root pom dependency-management block,federation-graphql-java-support6.0.0 → 6.2.0 andgraphql-java-extended-scalars22.0 → 24.0, holdinggraphql-javaitself at 25.0 (the 25 → 26 jump breaks generator compilation and outruns what the two satellites officially support; deliberately out of scope as the conservative, ready-now half of the graphql-java catch-up). Federation 6.2.0 (built against graphql-java 25.0) and extended-scalars 24.0 both resolve cleanly and introduce no failures. R464 having already removed the convention table and its drift guard, the six new extended-scalars constants (YearMonth,Year,AccurateDuration,NominalDuration,SecondsSinceEpoch,HexColorCode) need no map-or-exclude curation: the reflective@scalarTyperesolver binds any public-staticGraphQLScalarTypeconstant on the classpath automatically, so they become resolvable candidates with no code change. Pure two-line version bump; no generator, generated-output, or test changes, exactly as scoped. A tree-wide grep confirms no live source, test, or user-facing doc still references the deleted convention table or itsconventionTable_coversEveryExtendedScalarsFielddrift guard. Independent-session In Review → Done review; full reactor green under-Plocal-dbacross all 13 modules. -
R346 (
ab70c35/41c0b70): Regenerate and guard the generatedsupported-directives.adocmigration fragment against directive-set drift. The fragment (included bymigrating-from-legacy.adoc, emitted bydirective-support --mode=migration) promised in its own header a "verify-mode CI guard" that never existed, so it drifted silently every time a directive changed. Thedirective-supportCLI gains an additive--verifyflag mirroringleaf-coverage --verify: it regenerates the fragment in memory, compares against the committed--outputfile, and throwsBuildFailurewith a copy-pasteableREGENERATE_COMMANDon any drift (--verifyrequires--output, else exit 64). To keep the verify path inside the rewrite tree (standalone-build principle), the frozen 25-directive legacy set is snapshotted verbatim fromgraphitron-common/src/main/resources/directives.graphqlsat tag v9.3.0 (pre-R182 delete) intoroadmap-tool/src/main/resources/legacy-directives.graphqlsrather than read from the legacy module; a stripped-before-parse provenance header documents the source. A phase-boundverify-supported-directivesexecution inroadmap-tool/pom.xmlruns the guard on everymvn verify(PRs included), alongsideverify-roadmap-readmeandcheck-adoc-tables, so drift fails on the PR that introduces it rather than after merge. The one-time regeneration corrects the generated Supported-directives prose (R346 decision 2): it drops the unbacked "exercised by … a test fixture" claim (migration mode never computes that signal into the list, which gates onrewriteByName.containsKeyminus the rejected/withheld sets) and states the real criterion (declared + supported, documented in the architecture chapter). Coverage:DirectiveSupportReportTestgains the prose assertion plus the--verifyround-trip (zero on match,BuildFailureon drift, exit 64 without--output); the guard is its own regression net. The siblingsupported-schema-shapes.adocfragment (identical unguarded gap, but a build-trace-dependent guard shape) is deferred to a Backlog follow-up. Independent-session In Review → Done review; guard verified to fail cleanly on a deliberate fragment edit; full reactor green under-Plocal-db. -
R464 (
7786ce4, Specf79855e/b1ff238): Remove convention-based scalar resolution; make@scalarType(scalar: "fully.qualified.Class.FIELD")the single explicit way to bind any non-spec, non-federation scalar. Generator side is pure deletion:ScalarTypeResolverlosesCONVENTION_TABLE,conventionTable(), andresolveByConvention(), andTypeBuilder’s classification ladder loses its convention rung so a directive-less non-spec/non-federation scalar falls straight to the `@scalarType-pointing hard error (message simplified to name the directive as the single fix). This deletes the classpath-dependent classification surprise (a transitive extended-scalars pull silently resolving a barescalar BigDecimalwith no directive and no intent) and retires the convention-table drift test that gated extended-scalars / graphql-java upgrades (it tripped on the extended-scalars 24.0 dry run and blocked R465). The five spec built-ins and the federation-namespace scalars are untouched. Rather than dropping editor support, the LSP completion is upgraded:ClasspathScannergains areadScalarConstantspass readingpublic static GraphQLScalarTypefields by exact JVM descriptor match (theJOOQ_CONDITION_DESCRIPTORidiom;finaldeliberately not required since the reflective resolver binds a non-final constant just as well), surfaced on a newCompletionData.ExternalReference.scalarConstantsslot (with a back-compat 5-arg constructor keeping ~20 test callers compiling);ScalarTypeCompletionsnow completesclassName.fieldNamefrom that scan, so it offers the consumer’s own scalar constants (com.example.Scalars.MONEY) plus any library’s with no coupling to extended-scalars, preferring a case-insensitive field-name match for the enclosingscalar X. The sakilascalar BigDecimaland theGraphitronSchemaBuilderTestR355 fixtures gain explicit@scalarType; the twoCONVENTION_LAYERclassification cases are deleted, the single-pathUnclassifiedTypecontract case survives (asserting the new message so a re-added fallback fails loudly), and the oldDIRECTIVE_BEATS_CONVENTIONcase is retained reframed asDIRECTIVE_ALIASES_TO_DIFFERENT_CONSTANT. Coverage: scanner test pins the public+static+GraphQLScalarTypefilter with three negatives (wrong field type, non-static, non-public), rewritten LSP completion + text-edit tests feed a populatedCompletionDataand assert theclassName.fieldNameitems, field-name-first preference, case-insensitivity, and the empty-on-other-directive guard; no code-string assertions on generated bodies. Docs sweep:custom-scalars.adoc,scalarType.adoc, andcode-generation-triggers.adocdrop the convention layer, and resolution order is now two implicit paths (spec built-ins, federation-namespaced) plus the directive. Pre-1.0 breaking change; the actionable error message points at the mechanical fix. Independent-session In Review → Done review; full reactor green under-Plocal-db. Unblocks R465. -
R459 (
ed5b79b):CompileDependencyGraphBuildernow models the schema-shape → fetcher wiring edge for fetcher-owning plain-object nesting types. A nested type that owns a fetcher (any classified nested field, per R303) emits a<Type>Fetchersclass its<Type>Typeschema-shape wires (FilmMetaType → FilmMetaFetchers); the builder registered only the schema-shape node, so the wiring loop never added the edge and theTypeSpecReferenceWalkcompleteness oracle (correctly) flagged the superset gap. Fix is a dedicatedaddNestedFetcherNodes()walk (called frombuild()beforeaddBlanketAndWiringEdges) that mirrorsTypeFetcherGenerator.collectNestedFetcherClasses’ reachability verbatim: iterate `TableBackedTyperoots, walk eachNestingFieldtree, dedup nested types by name, recurse into innerNestingField`s unconditionally, and register `units.fetchers(name)gated on a mirrorednestedTypeOwnsFetcherspredicate (noFetcherEmittercoupling, same discipline as the builder’sfiltersDecodeNodeId/hasSqlGeneratingFieldmirrors). Registering the node is the whole fix: the existing wiring loop then suppliesschemaShape → fetcher,schemaClass → fetcher, and the blanket edges for free. Coverage: unitCompileDependencyGraphBuilderTest.fetcherOwningNestingTypeRegistersFetcherNodeAndWiringEdgesand a pipelineIncrementalCompileHarnessTestcorpus extension (Film.meta: FilmMeta { language: Language @reference }, single-valued inlineTableFieldthat exercises exactly the wiring edge and not the deferred per-field gap). Independent-session In Review → Done review verified the fixture is non-vacuous (disabling the walk reddens the oracle with exactly the oneFilmMetaType → FilmMetaFetchersgap, nothing else); full reactor green under-Plocal-db. The nested fetcher’s own outgoing per-field edges are deferred to Backlog R462 (nested-fetcher-outgoing-field-edges). -
R457 (
7588358, corec2e818b, cutoverc41752d, gate-fix376ecd8, In Reviewe83532b):@mutation(table:)names a@mutation(typeName: DELETE)field’s write target on the consuming field, retiring@table-on-input for DELETE and making R332’s deprecation warning actionable. A DELETE commonly returns a bareID/Boolean/ count and can never return the deleted row’s@tabletype (R287 rejects DELETE →@tableat authoring time, backstopped byMutationDeleteTableField’s compact constructor), so the only place to name the write target is the field: `deleteFilm(in: FilmDeleteInput!): ID @mutation(typeName: DELETE, table: "film"), the field-level analogue of@service(argMapping:). Precedence is@mutation(table:)(preferred) > the input’s@table(deprecated migration bridge, silently outranked when both are present, never cross-checked). The spec’s rung 1 (return-derived table) was dropped by author/user agreement precisely because R287 makes it unbuildable for DELETE; rungs 2 (override) and 3 (bridge) ship and fully satisfy the goal. Mechanism:directives.graphqlsgainstable: Stringon@mutation(documented DELETE-only);DmlWalkerInputArgResolutiongains aRawArgarm making "the single input arg is not aTableInputType`" a normal outcome rather than an immediate reject, with UPDATE translating it back to the pre-R457 rejection verbatim (byte-identical) via `rawArgUpdateRejectionwhile the DELETE classifiers own the fallback;FieldBuilder.resolveDeleteWriteTargetresolves the table by precedence and re-derives the input fields throughTypeBuilder.resolveInputFields(factored out ofbuildTableInputTypeso both routes share one classification loop, returning the narrow(fields | failure)fact, not a synthesizedTableInputType); the R330 validator-bypass is closed by mirroringGraphitronSchemaValidator.collectInputFieldRejectionsat the field-derived call site (a field-derived input never lands in the registryvalidateTableInputTypewalk);table:on a non-DELETE verb rejects loudly with the typed, sealedMutationTableArgError.UnsupportedVerb(stablegraphitron.mutation-table-arg.*LSP code), with the classifier andmvn graphitron:validatereading oneTABLE_ARG_SUPPORTED_VERBSset. Commit 1’s R332 DELETE carve-out is repurposed at cutover (additive-then-cutover, no dead set) from suppression to selecting the DELETE-specific replacement wording, so the warning now names@mutation(table:). Coverage: pipeline-tierMutationTableArgClassificationTest(7 cases: byte-identical carrier vs@table-on-input, no-@tableclassifies, unknown-table reject, no-write-target message leads with@mutation(table:)and cites R287, INSERT/UPDATE unsupported-verb rejections, validator-mirror parity on both paths),TableOnInputDeprecationWarningTest, execution-tierFixtureWarningsGateTest(FilmDeleteInput warns naming@mutation(table:)), and thedeleteStorageBinByCoderound-trip inDmlBulkMutationsExecutionTest(the sakila fixture dropped@tableand set@mutation(table: "storage_bin"), round-trips identically against PostgreSQL); drift guardsRejectionSeverityCoverageTest+SealedHierarchyDocCoverageTest; no code-string assertions on generated bodies. Docs:mutation.adoc(signature, parameter table, "Naming the DELETE write target" section, reworked constraint bullet),table.adocWARNING,deprecations.adoc,code-generation-triggers.adoc,typed-rejection.adoc. A DELETE-scoped slice of R97’s "the write target is the consuming field’s property" axis; R97 remains the home for the general@table-on-input removal. Independent-session In Review → Done review; full reactor green under-Plocal-db(MutationTableArgClassificationTest7,TableOnInputDeprecationWarningTest4,FixtureWarningsGateTest2,DmlBulkMutationsExecutionTestround-trip all pass). -
R455 (
f1d93a8, workstream B319e668, In Review991c2ae): Fix theTypeSpecReferenceWalkblind spots that silently falsified the R410 incremental-compile completeness oracle’s superset guarantee (walkEdges(u) ⊆ modelGraph.directReferences(u)), so the dev-loop incremental compiler could prune a dependent an ABI change should have recompiled while the oracle stayed green. Two coupled workstreams, landed model-first so every trunk commit stays green. Workstream A (graphitron-javapoet):TypeSpec.referencedClassNames()now descends$Largs that are a nestedCodeBlock/ anonymous-classTypeSpec/AnnotationSpec(a$Tstored opaque in a$Lblock was previously lost, missed across 60+ emitter sites) and walks type- and method-level type-variable-bound declarations, guarded by an identity-visited set so self-referential bounds (T extends Comparable<T>) terminate; the javadoc is corrected so the sole remaining blind spot is a class name baked into a raw string ($LString/$S). Workstream B (CompileDependencyGraphBuilder): a separate top-down projection walk mirroringTypeClassGenerator’s emit seam models the type-to-type projection-composition edges that were entirely absent (`types.Film → types.Languagefor each inlineTableField/LookupTableFieldcomposingTarget.$fields(…)), attributing every edge to the hosting type class so nesting-hosted fields land on the outer type (not their immediateparentTypeName()), reachingNodeIdEncoderprecisely when an inline filter decodes a@nodeIdargument, adding the generated<Type>Conditionsedge for aGeneratedConditionFilter, and blanketing the frozenGraphitronClientExceptionscaffold. The per-child dispatch is a no-defaultexhaustive switch over theChildFieldleaves carrying the one-model drift guard (a future inline-projecting leaf fails to compile until its edge is declared).TypeSpecReferenceWalk’s javadoc documents the shrunk review-only residual (same-package raw code-bearing string) with a discovery recipe; net 2’s FQCN scan is kept as-is (a same-package simple-name literal scan would over-collect). The demonstrated false green (`Film → Languageinline projection on the R410 harness) is now true-green, and the harness corpus is extended with an inline@nodeId-decoding list reference (Language.films) exercising thetypeClass → NodeIdEncoder,→ conditions, and same-package nested-$Lprojection edges end-to-end. The fetcher-owning-nesting-type wiring gap the fix would otherwise surface is filed separately as R459. Coverage: 7TypeSpecReferencedClassNamesTestunit tests (nested$Lat depth ≥ 2, anonymous class, annotation, type/method type-variable bounds, recursive-bound termination, raw-string residual), 4 newCompileDependencyGraphBuilderTestunit tests (projection target,GraphitronClientExceptionblanket,NodeIdEncoderonly-when-decoding, nesting-hosted attribution to the outer class), and the extendedIncrementalCompileHarnessTest;MultiSchemaPipelineTest’s R78 guard gets deeper coverage for free and stays green. Independent-session In Review → Done review; oracle + builder + multi-schema suites green under `-Plocal-db, javapoet unit tests verified passing on demand. -
R456 (
80a3df7, In Reviewe8571cc): GuardWorkspaceFilesource/tree reads against a concurrentdidChangeedit/swap/close. The LSP handed the live, mutableWorkspaceFileout of its lock (Workspace.get) and let the five async request handlers (hover, completion, codeAction, definition, inlayHint, all onForkJoinPool.commonPool) plus the diagnostic-recalc drain walk its tree-sitter tree and read itsbyte[] sourceon pool threads whiledidChangeedited the byte array, swapped the tree, and eager-close()`d the previous native tree on the dispatch thread; the fields were plain (non-`volatile, non-synchronized). That raced a walk of a freed native tree (arenaIllegalStateException/ use-after-free killing the request), a torn(source, tree, version)triple soNodes.textextracted garbage and hover/diagnostic ranges were wrong, and worst aWorkspaceEditcomputed against mismatched offsets that the client applies and corrupts the user’s schema file (completion could even tear against itself across an interleaved edit). Design (settled at Spec via the principles-architect consult): copy-on-read snapshots scoped by the workspace. New immutableFileSnapshot(tree, source, version)record carries ats_tree_copyclone (jtreesitter’s documented cross-thread mechanism) whose native lifetime is independent of the live file’s, so the eagerprevious.close()and in-placetree.edit()on the dispatch thread stay exactly as they were and cannot invalidate a reader’s clone;WorkspaceFile.snapshot()captures the triple under theWorkspacelock (the same lock the mutators run under, giving the happens-before edge, so novolatileon the plain fields).FileSnapshotis a deliberately distinct type fromWorkspaceFilewith no shared read interface, so the compiler (not convention) enforces "safe to read off the dispatch thread" and a call site cannot rebind the mutable instance.Workspaceexposes lambda-scoped accessors, not the snapshot’s lifetime:withView(uri, absent, present)snapshots one file (or short-circuits toabsent),withAllViews(present)snapshots every open file under one lock acquisition into an orderedMap<String, FileSnapshot>so a composed cross-documentWorkspaceEditis computed against one consistent generation of the whole workspace; both close every clone (including partials taken before the lambda throws) in afinally, making leak-by-omission structurally impossible (jtreesitter registers noCleaner, so an unclosed clone leaks native memory until process exit) and keeping the native lifecycle in the imperative shell. The live-file handoutWorkspace.getand the now-orphanedopenUrisare removed from the public surface;WorkspaceFileno longer escapesWorkspace. All reader call sites migrate: the five async handlers wrap theirsupplyAsyncbodies,publishDiagnosticsForRecalculatesnapshots per drained URI,CodeActions.computeandIntraSchemaDefinitions.computeusewithAllViewsfor a consistent cross-document generation, and featurecompute()signatures switch fromWorkspaceFiletoFileSnapshot(accessor names match, so mechanical); the completion self-tear is fixed since one snapshot now feeds its position, directive, and value reads.FileSnapshotdeliberately omitsdeclaredTypes()/dependsOnDeclarations()(consumed only byWorkspace’s own under-lock mutators, never off-thread). Coverage: `FileSnapshotConcurrencyTestis the named enforcer (plain JUnit ingraphitron-lsp; the generator’s four-tier taxonomy does not cover LSP concurrency and these need no catalog) ; a snapshot stays walkable and pre-edit-consistent (text +version) afterapplyEditandreplaceContenteager-close the original tree, close is independent in both directions, andwithAllViewspins one generation across a concurrentdidChange; the existing handler/feature tests are the behaviour oracle for the mechanical migration. Tests mint snapshots through a package-localWorkspaceFileTestSupportbridge to the package-privatesnapshot(). Alternatives rejected: an immutableWorkspaceFilebehind avolatile(needs a home for the reused incremental-parseParserand refcounting/Cleanerfor old-tree lifetime, more churn same result), and serialising requests against edits per document (holds the lock across slow feature computation, kills request concurrency). No user-visible surface (no protocol/goal/directive change), so no user-doc draft. Establishes the safety precondition for R347 Slice 5’sdidCloseclose()(which must land on top of this, where closing the live tree cannot invalidate any snapshot); test 1 is the mechanical enforcer that keeps this true regardless of commit order. Independent-session In Review → Done review; fullgraphitron-lspsuite green (468 tests, 0 failures) under-Plocal-db. -
R452 (
723266d, Specc175917, Ready91310e7): Reject explicit@referenceand same-table participants on multi-table interface/union child fields, closing a silent-wrong-data hole. A single-cardinality multi-table polymorphic child field whose join path was an explicit@reference(condition or multi-hop) or whose participant shared the parent table built green yet returned an arbitrary participant row per parent:MultiTablePolymorphicEmitter.branchParentFkWherereturnednullfor any non-single-hop-FK shape and the caller lowerednullto "no WHERE", so every parent resolved the field to the same wrong data with no error at any tier (the batched list sibling failed loud on a blind cast, proving the single arm was the odd one out). The fix is a classification-time gate plus a type lift, both landing at the single choke pointFieldBuilder.resolveChildPolymorphicJoinPaths(all four producers: interface/union × table-backed/record-backed parent). Rule 1a rejects any field-level@referencestructurally (a single stated path applies the same hops to every participant, so it is terminal-correct for at most one and cannot express a distinct join per participant; author-correctable by removing the directive); rule 1b rejects a same-table participant as aDeferredcapability (participant table equals parent/hub →parsePathskips FK discovery → empty path → no correlation derivable; a self-FK participant is a legitimate schema, not an author error); rule 1c wraps zero/multi-FK auto-discovery failures with multi-table-child context sofkCountMessage’s generic "add a `@reference`" steer no longer leads straight into rule 1a. Both deferred sites point at the follow-up capability item (`per-participant-multitable-child-join-paths, filed as R458). The type lift changes the per-participant carrier onChildField.InterfaceField/UnionFieldfrom a rawMap<String, List<JoinStep>>toMap<String, ParticipantFkPath>, a new record carrying the resolved single-hop FK column-pair slots with a non-empty invariant enforced at construction: the classifier decides "supported shape" exactly once and the emitter cannot represent an unsupported one, retiringbranchParentFkWhere’s `instanceof/null-for-unsupported arm andbatchedBranchJoinPredicate’s `(On.ColumnPairs) JoinStep.Hop) path.get(0.on()blind cast (nullsurvives only for the legitimate root-fetcher / participant-absent case). The single-hop-FK shape predicate is single-sourced (singleHopFkColumnPairs) across the multi-table arm and the single-tablevalidateSingleHopFkJoin, and the stalestub-interface-union-fetchers.mdpointer (R36 shipped) is dropped. Docs:polymorphic-types.adocandmultitableReference.adocnow state the rejection and point at the deferred capability. Coverage: new pipeline-tierMultiTableChildReferencePathRejectionPipelineTest(9 cases: condition/multi-hop/single-hop-{key:}@referenceall reject identically by directive presence, union + record-backed-parent producer arms, same-table deferred rejection, zero-FK/multi-FK context wrappers, and the auto-discovered control carrying a non-emptyParticipantFkPath);TypeFetcherGeneratorTestfixtures migrated to the new carrier. Independent-session In Review → Done review; full reactor green under-Plocal-db -P!docs. -
R453 (
c529322, Spece9cb78d): Reject sort-enum values that declare neither@ordernor@indexinstead of silently skipping them. A partially-annotated sort enum bound to@orderBybuilt cleanly, contradicting the docs' promise of a per-value build failure (sort-results.adoc,order.adoc,orderBy.adocall state a missing value "fails the build with a per-value diagnostic"); at runtimeOrderByResolvercontinue`d past the unannotated value, and a request selecting only such values generated an empty ORDER BY, which on a paginated connection made keyset pagination slice a nondeterministic set (rows duplicate or vanish across pages). The rejection lands at the parse boundary in `OrderByResolver.resolveOrderByArgSpec: it accumulates every value lacking both directives while iterating the enum and, if the set is non-empty, returns aResolved.Rejectedafter the loop, making the empty-ORDER-BY state unrepresentable in the model (namedOrderscomplete by construction). A newAuthorError.SortEnumMissingOrder(String enumTypeName, List<String> missingValues)arm carries the sort enum’s type name plus the full list of unannotated values (accumulate-all, not fail-fast, so the author sees every missing value at once; typed list rides for LSP fix-its rather than prose), following theRecordBindingMultiProducershape formessage()andprefixedWith. Per the Spec: the classifier’sanyMatchdetection stays (detection vs completeness are distinct facts; a fully-unannotated enum never reaches the resolver and is already rejected as unclassified); no validator mirror (the parse-boundary rejection makes the bad state unrepresentable, and the model carries no full value list to re-derive); no generated-code guard (the resolver rejection is the single enforcer; emitted code carries no defensive guards for classifier-guaranteed shapes); and the annotated-value catalog-lookup fail-fast path is left untouched as a distinct failure class. No user-manual change (the docs already promise this exact failure; the change aligns code with the promise). Coverage: pipeline-tierGraphitronSchemaBuilderTest(partial annotation → build error naming the unannotated value; two unannotated values → single accumulate-all rejection listing both), plus drift guardsRejectionSeverityCoverageTest(sample for the new arm) andSealedHierarchyDocCoverageTest(typed-rejection.adocparagraph + drift-list mention); no generation-tier assertion (code-string assertions banned, the guard it would pin is dropped) and no execution-tier test (the nondeterministic state is now unbuildable). Sibling of R181 (validate-order-directive-args, the empty-@order/@order+@index-coexistence gap); the arm is named so R181 can fold into a shared order-directive family later. Independent-session In Review → Done review; full reactor green under-Plocal-db. -
R449 (
89746df, In Reviewdc70e62): Close the routine-chain classification edges surfaced by R435’s second-pass In Review review. Five design points, none gating R435’s shipped surface: (D1) gate the R435 root-chain interception on Query ; the interception readparentType instanceof RootTypeand routed root multi-node chains toclassifyRootRoutineChain, so a@routinechain onMutationlanded aQueryRoutineTableFieldwhosesource()falsely assertsRoot.Query;FieldBuilder.classifyFieldnow reads the root position once (isRoot/isQueryRoot/isMutationRoot) and a Mutation@routine(multi-node in the interception, single-node fromclassifyMutationField’s top) lands a typed `Deferredsignpostingroutine-mutation-write(R451’s write arm, a capability gap not an authoring error), while Subscription and non-routine Mutation chains fall through to theirclassifyRootFieldstories. (D2) fold@routineinto both conflict detectors via a pairwise verdict table ; a sealedPairVerdict(Conflict|Deferred(planSlug)|Composes) thatreduceDirectiveConflictprojects over every unordered pair and reduces with Conflict-dominates-Deferred precedence, so@routine @lookupKey @servicerejects the@serviceconflict rather than short-circuiting to the@routine×@lookupKeydefer (the three-directive hole a slot-count carve-out would reintroduce);detectQueryFieldConflictis hoisted intoclassifyFieldbefore the interception and its old call insideclassifyQueryFielddeleted (one detector site per position). (D4) repointBuildContext.computeTerminalTargetVerdict’s `On.Lateralcomment atFieldBuilder.routineChainVerdict(theSplitRowsMethodEmitterjavadoc repairs stay R450’s scope). (D5) route the root routine fetcher (TypeFetcherGenerator) through the sharedRoutineCallEmitter.emitCallvia a new payload-freePreviousNodeRef.Nonearm andJoinPathEmitter.emitTableExpression, deleting the duplicatednonRoutineParamSourcehelper and inlineParamSourceswitch;QueryRoutineTableField’s compact constructor now pins every start binding to `ParamSource.Arg, making theNone×SourceColumnarm genuinely classifier-unreachable (construction throws before any emit), and the consolidation is byte-identical (rootcorrelatedis false, so noDSL.valwrap). (D3) three text-only R435 rejection fixtures gainisInstanceOfarm assertions (DirectiveConflictfor repeated@referenceonARGUMENT_DEFINITION;AuthorError.Structuralfor the input-field and element-less cases) plus a new fixture pinning the R300 single-node root desugar tohops = []. Coverage: 11 new pipeline-tier fixtures inGraphitronSchemaBuilderTest’s R435 block (D1 Mutation/Subscription landings, D2 conflict/defer/precedence verdicts across child + root single-node + root multi-node, the desugar pin); D4/D5 need no new tests (comments have no runtime surface; the consolidation is behaviour-identical under the existing R435 pipeline + execution suite, and the D5 constructor pin is exercised by every fixture that lands `QueryRoutineTableField); no code-string assertions on generated bodies. Independent-session In Review → Done review; full reactor green under-Plocal-db(503GraphitronSchemaBuilderTestcases, 0 failures). Out of scope: the routine write arm itself (R451), R450’sSplitRowsMethodEmittercorrelation rework, and lifting root position into a sealedRootType(a model-cleanup follow-up). -
R446 (
03dfe0b+c57fc57): Fix codegen crashing on array-typed database columns and reject array columns used as key elements. The R436 per-columnTableRecordkey reconstruction calledClassName.bestGuess(col.columnClass())for every column inallColumns(), and for an array column jOOQ’sField.getType().getName()is the JVM binary descriptor ([Ljava.lang.Boolean;), whichbestGuessrejects, so any node type with an array-typed mapped column abortedgraphitron:dev/generatewithIllegalArgumentException: couldn’t make a guess for [Ljava.lang.Boolean;. Fixed at the grain: decide each column’s Java type once at the jOOQ reflection boundary and carry it as aTypeName columnTypeonColumnRef/JooqCatalog.ColumnEntry, decoded viaTypeName.get(col.getType())(array-safe, mirroringRoutineParam) at the three reflection sites and threaded through every construction site; the 31ClassName.bestGuess(<ref>.columnClass())codegen sites read<ref>.columnType(). The rawcolumnClassstring is left in binary form intact for the consumers that depend on it (EnumMappingResolverClass.forName,SourceRowDirectiveResolverClass.getName()compares,GraphitronSchemaValidator), so this is a dual-fact split, not a replacement.ClassName-typed locals andDomainReturnType.Plainwiden toTypeName(safe:TypeName.equals/hashCodearetoString()-based and a scalar column still decodes to aClassName, so R204/R279 multi-producer agreement is unchanged for scalars and distinguishes arrays). A validate-time rejection is added for an array-typed column used as a@nodeNodeId key column (validateNodeType) or a DataLoader@splitQuerybatch key (validateFieldoverBatchKeyField.sourceKey().columns()), because Java arrays compare by reference identity and would silently mis-batch / mis-match at runtime; merely "not throwing" there would turn a build-time crash into a silent correctness bug. A test-only 3-/4-arg auxiliary constructor derivescolumnTypefrom a source-formcolumnClass(shared scalar-only decode,nullon placeholders, array columns can’t reach it) so ~161 hand-built fixtures stay unchanged;ColumnTypeConstructorArityGuardTestscans the main-source tree and fails the build if any production construction regresses to the string-decoding form. Coverage: unit-tierArrayColumnTypeDecodeTest(boundary decode:ClassNamefor scalars,ArrayTypeNameof the right element forboolean[]/text[],columnClassstays the binary descriptor), pipeline-tierArrayColumnCodegenPipelineTest(regression pin over a newarray_holderfixture through theTableRecordkey-extraction path, verified to reproduce the crash when reverted), andArrayKeyColumnRejectionValidationTest(@nodearray-key rejection + scalar control); no code-string assertions on generated bodies. Compilation/execution tiers not added via sakila-example (array columns aren’t surfaced there; emitted-form validity is pinned by the boundary-decodetoString()), and the DataLoader batch-key rejection arm has no fixture (unconstructable: PostgreSQL disallows an array FK target). Bug fix, regression from R436; no user-facing surface. Independent-session In Review → Done review; full reactor green under-Plocal-db. -
R435 (impl
c0620e5..7a58239, In Reviewe288670): Order-significant@routine/@referencecomposition ; a jOOQ table-valued function is a table node (R333’stableExprRoutineCall) that can be a field’s row source, its projected terminus, or sit between tables in the join chain. Both directives becomerepeatableand their written order defines the table chain (implicit head → contributions → terminus, last node == the field’s@table);ReferenceElementgains no arm and@referenceno new luggage, so composition is directive co-occurrence plus order, not a widened input (the rejected-alternatives log records why the@oneOfelement arm, a slim path directive,@reference(from:), and a fixed-orientation rule were all turned down). Discharges R333’s deferred SDL-surface residue and its root-entry-validator residue (b). Model: on R438’s two-axisHop(TableExpr target, On on)substrate, addsTableExpr.RoutineCall, the positiveOn.Lateralarm,On.Keying(ForeignKey|NameMatchedKey, the FK-less name-matched key for hops adjacent to a routine result),ParentCorrelation.OnLateralArgs, and aParamSource.SourceColumnarm so a routine IN parameter has exactly one source shape (argMapping→Arg,columnMapping→SourceColumn, the correlatedCROSS JOIN LATERALcase); every pre-existing sealed switch onOn/ParentCorrelationgained an explicit arm and the rootQueryRoutineTableFieldwas re-homed onto the(start, hops)chain (R300 desugars tohops = []). Shipped end to end at root and child positions: correlated single-node (lateral), routine-then-hops, hops-then-routine, sandwich, and repeated-@referencecomposed chains, through one chain walker shared by root and child classifiers; inline correlated multiset and the@splitQuerybatched keyed re-query on table-backed parents (batch key = the routine’s column-bound inputs). Typed classify-time rejections (root-head, terminus,columnMappingexistence/type/one-source, repeated@referenceoff FIELD_DEFINITION, Connection-terminus, uncorrelated-@splitQuery) each carry a validator projection and a fixture;@orderBy/@condition/ catalog-terminus-pagination /@lookupKeyland typedDeferred. Coverage: pipeline fixtures per chain shape inGraphitronSchemaBuilderTest’s R435 block, execution-tier proofs on a `films_for_actorfixture function (RoutineFieldExecutionTest, per-parent correlation, mixed column/argument binding, batched-vs-inline row equivalence, hops projecting a film-only column so a mis-keyed hop cannot pass), and the full rejection-fixture set; no code-string assertions.routine.adoc/reference.adocrewritten for the composition surface and order contract. Remaining fetch-form breadth (multi-routine chains, record-backed /TableInterfaceTypeparents,@lookupKey) re-homed to R447; ordering/binding/corpus residue to R448. Independent-session In Review → Done review; full reactor green under-Plocal-db(3802 tests). Builds on R333/R438; supersedes R300’s root-only@routine. -
R445 (
579ec14, In Review39ecbb9): Resolve a participant cross-table@referencecolumn read by the FK-pinned terminalTableRef(class identity), not a bare SQL name re-resolved through the catalog. Seventh audited site of the schema-qualified@tablebug class (siblings R396, R440, R441, R442, R422, R444), found by R444’s spec-time audit.TypeBuilder.extractCrossTableFieldsheld the FK-pinnedfk.targetTable()(identity-carrying since R441) and the resolvedinterfaceTable, but re-resolved their baretableName()strings throughJooqCatalog’s string lookups, which return `TableResolution.Ambiguouswhen the FK terminal’s bare name collides across generated schemas: the column resolve came back empty and the field was silently skipped from the participant’s cross-table set, falling through toFieldBuilder’s scalar `@referencepath where (post-R444) it misclassified as a plainColumnReferenceFieldinstead ofParticipantColumnReferenceField, so the interface fetcher emitted no conditional LEFT JOIN / alias projection and a participant field’s classification came to depend on whether an unrelated schema happened to hold a same-named table. No author-side workaround: the FK terminal is not author-named (the@referencekey isTABLE__CONSTRAINTon the source table), and this path never routes throughServiceCatalog.resolveColumnForReference, so R444’s overload retirement could not catch it. Design (same as R440/R441/R422/R444, "decide once, carry the decision as a type"): consume the carried refs directly via R444’sTableRef.column(String)matcher andTableRef.allColumns(), retiring all four string-keyed catalog reads from the method ; R388 defect-2 guard predicate + base column set →interfaceTable.column/allColumns; detail-only candidate hint →fk.targetTable().allColumns(); column resolve →fk.targetTable().column(columnSqlName), dropping the manualColumnEntry→ColumnRefconversion.extractCrossTableFieldsnow holds zero string-keyed catalog reads, one resolution story instead of two. Behavioral deltas: a colliding FK terminal now yields aParticipantColumnReferenceField(the fix), and the R388 rejection’s candidate hint is non-empty when the detail table collides; unknown columns still skip to the field-level classifier and the guard still fires on base-resident columns. Coverage: pipeline-tierQualifiedParticipantCrossTableReferencePipelineTest(sibling of R444’sQualifiedTerminalReferenceColumnPipelineTestand R422’sQualifiedReturnTypeReferencePipelineTest, over the existing multischema fixture, no new DDL) ; a cross-schema-colliding FK terminal classifies green asParticipantColumnReferenceFieldwith the resolved column asserted (not vacuous); a column present only on the other schema’s same-named table still rejects (schema-pinned, not search-all-schemas), its diagnostic naming the FK-pinned A-side candidates; a base-resident column trips the R388 contradiction guard with a now-non-empty detail-only candidate hint. Out of scope by design:ctx.parsePath’s path-start `tableName()echo (author-named and qualifiable per R396). Independent-session In Review → Done review; red-before-green verified (tests 1 and 3 fail with the fix reverted); full graphitron suite green (2489 tests) under-Plocal-db. Closes the audited no-workaround FK-terminal@referencesub-class (R444 scalar + R445 participant cross-table); does not claim the whole schema-qualified@tableclass closed ; the author-qualifiable bare-name reads (ServiceCatalog.resolveColumn, path-start echoes) remain by design. -
R444 (
e5f3944, In Review3785229): Resolve a scalar@referenceterminal column read by the FK-pinned terminalTableRef(class identity), not a bare SQL name re-resolved through the catalog. This was the sixth site of the schema-qualified@tablebug class (siblings R396, R440, R441, R442, R422): despite R422’s changelog claiming the class closed, R422 only fixed the object-return-type terminal verdict; the scalar@referencecolumn read was a separate unaudited path with the identical defect.ServiceCatalog.terminalTableSqlNamewalked the FK path and returnedhop.targetTable().tableName(), collapsing the identity-resolved terminalTableRef(which R441 populated with atableClassidentity) to a bare name string;resolveColumnInTablethen re-resolved it throughJooqCatalog.findColumn(String, …), which hitTableResolution.Ambiguouswhen the terminal table name collides across generated schemas and demoted the field toUnclassifiedFieldwith a spurious "column could not be resolved" author error and no author-side workaround (the@referencekey names the FK on the source table, so there is no syntax to qualify the FK terminal). Design (same as R440/R441/R422, "decide once, carry the decision as a type"): replaceterminalTableSqlName/terminalTableSqlNameForReferencewith the ref-carryingterminalTableForReference(List<JoinStep>, TableRef)(walk the path, terminal is the last hop’stargetTable(), empty path yieldsstart, condition-only steps still bail to empty); retyperesolveColumnForReferenceto take the start asTableRefand resolve the column off the terminal ref via newTableRef.column(String); the single model-side matcher home mirroringfindColumn’s order (`javaNameequalsIgnoreCaseacross all columns, thensqlName), never a bare-name catalog re-resolve. The scalar-output unknown-column diagnostic now enumerates the terminalTableRef.allColumns()java names (previously empty on a colliding terminal, ambiguity-broken too); the argument-filter (FieldBuilder) and input-field (BuildContext) sites pass their already-resolvedTableRef. Behavioral deltas: a colliding terminal now resolves; condition-only paths, unknown columns, and empty paths keep today’s outcomes. Coverage: pipeline-tierQualifiedTerminalReferenceColumnPipelineTest(sibling of R422’sQualifiedReturnTypeReferencePipelineTest, over the existing multischema fixture, no new DDL) ; a@referenceread on the cross-schema-colliding FK terminal classifies green asChildField.ColumnReferenceField(with the resolved column asserted, not vacuous); a column present only on the other schema’s same-named table still rejects (schema-pinned, not search-all-schemas); a genuine unknown column rejects with a non-empty candidate list. Two adjacent bare-name reads stay out of scope by design: the direct non-@referencescalar read (source@tableecho resolves qualified, author has a workaround) and the participant cross-table@referencepath (a genuine seventh site, no workaround, tracked as R445, which consumes this item’sTableRef.columnmatcher). Independent-session In Review → Done review; full graphitron suite green (2526 tests) under-Plocal-db. Closes the FK-terminal@referencecolumn-read sub-class (scalar output field, argument filter, input field); does not close the whole bug class (the participant cross-table path is R445). -
R422 (
6955bf6, In Reviewa1edbe0): Compare return-type identity in the@referenceterminal-target verdict, not the verbatim@tableecho.BuildContext.computeTerminalTargetVerdictdecided whether an@referencepath’s terminal hop lands on the field’s return-type table viaTableRef.sameTable(a bareequalsIgnoreCaseagainst the return type’s verbatim@tablestring), so a schema-qualified return@table(e.g.multischema_a.widgetwhile the hop resolves to jOOQ’s unqualified canonicalwidget) spuriously reportedMismatchand demoted the field toUnclassifiedField. Last open member of the schema-qualified@tablebug class after R396 (source-side FK predicate), R440 (FK-join endpoint/FK identity), R441 (landedTableRef.denotesSameTableAs), R442 (condition-param match). Chosen design (settled at Spec against a fullparsePathString→identity migration): thread the already-resolved return-typeTableRefintoparsePathas its own nullable axis alongside the existingtargetSqlTableNameString ; name stays the input to the name-based plumbing (empty-path FK inference, condition-join terminal build), the ref is consumed only by the verdict, which now compares via R441’sTableRef.denotesSameTableAs(both sides catalog-constructed, so identity-vs-identity). The seven non-null-target call sites pass the ref they already hold one frame up (six inFieldBuilder, plusNodeIdLeafResolverwhich hoists itsfindTableaboveresolveFkJoinPathso ref and name pass together); the return-side null gate moves toreturnTableRef == nulland theMismatchmessage keeps rendering the author’s verbatim echo. No R440-style resolve-or-fall-back contract (an unresolvable return name is unreachable here and re-resolving could reintroduce the bug on a bare cross-schema collision). Coverage: pipeline-tierQualifiedReturnTypeReferencePipelineTest(sibling of R396’sQualifiedSourceReferencePipelineTestover the multischema jOOQ fixture, no new DDL) ; the schema-qualified return@tableclassifies green asChildField.TableFieldand the terminal hop genuinely lands onwidget, paired with a genuine mismatch (return type bound toevent, hop lands onwidget) that still rejects toUnclassifiedField; no code-string assertions.TableRef.denotesSameTableAsalready pinned by R441’sTableRefSameTablePredicateTest, no new predicate coverage. Independent-session In Review → Done review; full reactor green under-Plocal-db. Builds on R396/R441; closes the schema-qualified@tablebug class. -
R440 (
d0bd8ca, Spec6cfda86/426c586): Resolve FK-join synthesis endpoints and the FK itself by jOOQ class / reference identity, not bare SQL name.BuildContext.synthesizeFkJoinalready held the jOOQForeignKeyobject (whose endpointTableclasses it pins exactly) but re-looked-up both endpoints and the FK by bare name, reintroducing the ambiguity R396 removed: two schemas sharing a bare table name yieldedTableResolution.Ambiguousand the join failed, and a constraint name colliding across schemas silently returned the first-hit FK (a wrong-join hazard, not a rejection). Another member of the schema-qualified@tablebug class (R396 done; R441/R442 the accessor and condition-param siblings landed just prior; R422 Backlog). Four moves, all "decide once, carry the decision as a type": (D1) newJooqCatalog.findForeignKeyRef(ForeignKey)resolves theKeys-class constant by reference identity in the FK-holder schema only (the FK-child endpoint class structurally pins the owning schema), the FK-singleton invariant its named enforcer;fkJavaConstantNameretargeted onto the FK object; the false-docstringfindForeignKeyByNamedeleted. (D2)synthesizeFkJoinresolves both endpoints viafindTableByClassoff the FK, soUnknownTablebecomes a defensive-only arm (fires on catalog-vs-FK mismatch, never bare-name ambiguity); the fabricated-source test case retired in favour of upstream membership checks. (D3) theOptional-returningfindForeignKey(String)(which could only collapse a collision into "not found") replaced by a scoped, sealedfindForeignKey(name, sourceSqlName)returningForeignKeyLookup(Resolved/NotInCatalog/Ambiguous); the three author-facing sites ({key:}path element, IdReference synthesis shim, explicit@reference(key:)record-FK) rejectAmbiguousthrough a newambiguousForeignKeyRejectionstructural builder naming the colliding schemas + qualified forms, while the non-author-facingqualifierForFkmigrates keeping itsOptionalcontract. (D4)findUniqueFkToTablereturnsOptional<ForeignKey>andNodeIdLeafResolverconsumes the object directly, dropping the name round-trip that re-collided afterfindForeignKeysBetweenTableshad already resolved by class. Fixture: anotetable in both multischema schemas, each with an FK namednote_event_fkinto its own schema’sevent, giving both a colliding bare target-table name and a colliding FK constraint name, plus seed rows for execution-tier reuse. Coverage: unit-tierJooqCatalogMultiSchemaTestD1-D4 cases (per-schemaKeys-class ref resolution, endpoint-by-class from each side, scoped disambiguation, null-scopeAmbiguousnaming both schemas, structural-prose rejection) plus the{key:}membership enforcerparsePathElement_keyNotTouchingSource_rejectsBeforeSynthesis;findUniqueFkToTabledirectionality cases migrated to the FK-object shape inJooqCatalogIdRefTest; no code-string assertions on generated bodies. Resolver-tier wiring judged disproportionate (nodeid fixtures stay single-schema; the collision lives at the catalog/BuildContexttier), recorded in the item.ForeignKeyLookupis aJooqCatalog-local result type in theTableResolution/ForeignKeyResolution/RoutineResolutionfamily, out of scope forVariantCoverageTest/SealedHierarchyDocCoverageTest; the ambiguous arm produces an existingRejection.structuralleaf, adding notyped-rejection.adocobligation. Independent-session In Review → Done review; full reactor green under-Plocal-db. Builds on R396/R438; sibling of R441/R442/R422. -
R442 (
87d25a9, Specb9c8cfa): Make the R379 Check-2 concrete-condition-param table match compare by jOOQ class identity, not a bare-vs-qualified name string.BuildContext.checkConcreteParamTablecompared the parameter’s bare jOOQ table name (event) against the hop’s possibly schema-qualified@tableecho (multischema_a.event) withequalsIgnoreCase, so in a multi-schema catalog a concrete@conditionparameter typed with the correct generated table class was false-rejected (author’s only workaround: widen toTable<?>, discarding the type safety), and two same-named tables in different schemas were indistinguishable. Another member of the schema-qualified@tablebug class (R396 done, R441 the accessor-side sibling landed just prior, R422/R440 Backlog); surfaced from gap D of theopptakmulti-schema migration. Design shape 1 (chosen over re-resolving the string inside the check): thread the resolvedTableRef`s down the validator chain instead of name strings. `validateConditionParamTables/checkConcreteParamTablenow takeTableRef source/target; the condition-hop site threads the hoistedconditionOrigin(null when the source is not table-backed, the existing skip) andr.target(), the where-filter site threadshop.originTable()/hop.targetTable(). The final compare isTableRef.denotesSameTableAs(the shared identity-body-with-name-fallback predicate R441 landed first and this item reused verbatim per the coordination note, dropping the predicate + consumer-audit scope), so a parameter typed with the right class classifies green even against a qualified echo and cross-schema collisions stay distinct; the mismatch message renders the declared side schema-qualified so a bare-name collision stays actionable. Coverage: pipeline-tierMultiSchemaConditionParamTest+MultiSchemaConditionStubover the colliding-eventmultischema fixture, covering source (terminal condition + where-filter) and target (terminal condition) operands, both green and by-identity wrong-schema rejection; the catalog-built wrong-schema cases double as the fallback-arm enforcer. Additivemultischema_a.event_logDDL (jOOQ schema version bumped 2.4→2.5) backs the where-filter source shape. Test-plan deviation recorded in the item + landing commit: target-side coverage rides a terminal condition hop rather than the spec’s where-filter hop, because the where-filter FK-endpoint resolveseventby bare name throughsynthesizeFkJoin(ambiguous across the two schemas), which is R440’s scope, not R442’s. Independent-session In Review → Done review; fullgraphitronsuite green under-Plocal-db(2471 tests). Builds on R379; reuses R441’s predicate.
Historical record of completed rewrite work. Entries are roughly reverse-chronological; commit shas and plan slugs are preserved for archaeology. The forward-looking ledger lives in README.md, generated from per-item front-matter in this directory.
The next-id: front-matter field is the canonical counter for R<n> allocation, maintained by roadmap-tool create. Numbers are never reused (see workflow.adoc); the counter advances past every Done so the gaps left by deleted item files don’t collide with future allocations.
-
R441 (
e0b878aimplementation; Backlog → Spec62d1222, Spec → Readyd5117fa, In Progress9d3756c, In Review2fdb708; independent-session In Review → Done review): Typed-accessor match on a free-form DTO payload parent now compares reified jOOQ table-class identity, not the bare@tablename. Same multischema migration as R396/R422’s family (gap E ofgraphitron-qualified-names-gaps): once an element type’s@tableis schema-qualified to disambiguate a cross-schema bare-name collision (multischema_a.eventvsmultischema_b.event), the verbatim@tableecho neverequalsIgnoreCase’d jOOQ’s always-unqualified canonical `event, soFieldBuilder.collectAccessorMatchessilently dropped the accessor and the payload parent rejected withRecordTableField … requires a typed accessor or @sourceRowplus cascadingWrapperArm errors transportfailures on the siblingerrorsfield. Fix routes the comparison through the identity already in hand:TableRef.denotesSameTableAs(TableRef)compares the carriedtableClass(ClassName, structural equals) when both refs have one, falling back to the case-insensitive name compare only for fixture-built classless refs (catalog-constructed refs viaJooqCatalog.TableEntry.toTableRefalways populate it, so production always takes the identity arm);collectAccessorMatchesthreads the expectedTableRefinstead of its bare SQL name and filters viadenotesSameTableAs, with the accessor side already resolved by record-class identity (ServiceCatalog.resolveTableByRecordClass) so the compare is identity-vs-identity. The predicate’s javadoc names the two identity homes (parse-boundary raw-Table<?>primitives onJooqCatalogper R396 vs. model-side reifiedClassNamehere) so a future consumer picks by where it stands rather than growing a third mechanism. Upgrading the predicate body silently switched the four other consumers (TypeBuilder×3,GraphitronSchemaValidator,FieldBuilderhop-origin) to identity comparison; each compares two same-catalog-derived refs, so any change of verdict is a cross-schema false-positive becoming correctly false (strictly tightening). Gap D stayed out of scope, filed as R442 which subsequently adopted the same predicate. Coverage: unit-tierTableRefSameTablePredicateTestpins all three arms (same class/divergent names → true; same bare name/different class → false, the silent-regression guard; classless fallback); pipeline-tierTypedAccessorSchemaQualifiedIdentityPipelineTestover the multischema fixture pins both directions (qualified echo classifies green asRecordTableFieldwith accessor-derived source and schema-AtableClass; different-schema record dropped and field rejects), asserting classifier outcomes andsourceKey, no code-string assertions on generated bodies.TableNameComparisonCaseGuardTestuntouched (identity route lives in the guard’s excludedPREDICATE_HOME). Full reactor green under-Plocal-db. -
R438 (
materialize-joinpath-facts; slices3754f40JoinConditionRef,8106991axes minted,099aa30producer/reader cutover,d3dafa3flat-variant delete,b0ab513self-review fixes; independent-session In Review → Done review):JoinStepreshaped onto R333’s two orthogonal axes ;Hop(TableExpr target, On on, originTable, JoinConditionRef filter, alias)withOn.ColumnPairs | On.Predicateand the day-oneTableExpr.Catalogarm; the flatFkJoin/ConditionJoinvariants and theWithTargetcapability deleted, slot iteration now the standaloneHasSlotscapability shared byOn.ColumnPairsand the transitionalLiftedHop(both retired by R431). Absorbed and closed R16. Generated output byte-identical per slice; gate review re-verified the full reactor green under-Plocal-dband fixed one staleWithTargetmention inemitter-conventions.adoc. -
R16 (
fkjoin-model-cleanup; absorbed and closed by R438’s slice 1 + cutover): the join-condition calling convention is now typed.JoinConditionRefwraps theMethodRefpopulation called asmethod(srcAlias, tgtAlias)byJoinPathEmitter.emitTwoArgMethodCall, which takes the wrapper directly so call sites stop extracting rawMethodRef`s; handing a `WhereFilter-convention method to a join-condition emit site is a compile error. R16’swhereFilternaming complaint dissolved structurally in the R438 cutover: the ON-clause condition and the WHERE-appended filter became differently-named components (On.Predicate.conditionvsJoinStep.Hop.filter) instead of one overloadedwhereFilter. -
R439 (
d34ef37implementation; filed66dea39, Backlog → In Reviewea67d6cat the user’s direction with the implementation pre-landed and validated in the originating session, the Spec/Ready flips recorded as mechanical passthrough; independent-session In Review → Done review): Background dev-environment warm-up for Claude Code Web sessions. The SessionStart hook (.claude/scripts/session-start-web-env.sh) now runs asynchronously in web sessions (CLAUDE_CODE_REMOTE=true): it emits{"async": true}so the session starts immediately, establishes the prerequisites (JDK 25 + alternatives/profile retarget, PostgreSQLrewrite_testdrop/reseed, Maven settings de-proxy, libtree-sitter 0.26.9) in the background, then warms the whole reactor withmvn -B -ntp install -P 'local-db,!docs' -DskipTests, tracking state in/tmp/graphitron-web-env.status(prereqs/warm-build/done/failed+ hook PID + epoch, with an EXIT/TERM/INT trap so a killed hook leavesfailed, never a stuck running state) and logging to/tmp/graphitron-web-env.log. A new PreToolUse Bash guard (.claude/scripts/wait-for-web-env.sh, registered in.claude/settings.jsonwith a 2700s timeout) extracts the command from the hook JSON and holdsmvncommands through both phases andpsqlcommands through the prereqs window, so a foreground build can never race the background one into the catalog-jar clobber; it fails open on a dead hook PID, a stale (>2400s) status, or an unparseable status file, and is also runnable by hand as a wait-with-log-tail. The JDK step additionally persistsJAVA_HOMEthrough$CLAUDE_ENV_FILEso agent shells stop inheriting a staleJAVA_HOME=java-21past the enforcer. Local sessions keep the fully synchronous behavior and never create the status file. Dev-tooling only, no generator code affected; gate review re-verified the guard’s hold/release/fail-open matrix against synthetic status files and observed the live warm-up (async start,warm-build→done, BUILD SUCCESS, JDK 25 in the agent shell) in the reviewing session’s own sandbox. -
R434 (
3b38bebimplementation; Spec → Readyee39933, In Progress → In Review3cac680; independent-session In Review → Done review): Restructured the rewrite design principles doc around axioms with named enforcement.rewrite-design-principles.adoc(28 flat peer sections, the type-system family stated five times, the central R222 thesis living only in a preamble pointer) is replaced bydocs/architecture/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 + anEnforced by:line naming what fails when it breaks (compiler / named meta-test / build tier / the honest gap label "review only", which doubles as the meta-test gap list). The ingress states the FCIS + normalization spine and coins the drift smell once (R268 narrated solely under the enforcement axiom; field/model/format instances cite it). The Emitter Conventions catalogue extracted todocs/architecture/reference/emitter-conventions.adoc(plus a new cursor encode/decode section); the dunder-rule full semantics moved toGeneratedSourcesLintTest’s javadoc next to their enforcer; additive-then-cutover change discipline relocated to `roadmap/workflow.adoc. The doc budgets itself at 3,500 words (landed at 3,456), enforced by newDocSizeBudgetTest.developmentPrinciplesStaysUnderBudget. Citation sweep retargeted every live reference (docs xrefs with changed section anchors, javadoc,.claudeprompts,CLAUDE.md, roadmap-tool boilerplate, live roadmap items); historical records keep the old name. Docs-only plus the budget meta-test; full reactor green under-Plocal-db. Builds on R433. -
R433 (
420cd32implementation,d905facrework 1,2ba655arework 2; Ready sign-off under the user’s in-session short-circuit, In Review → Ready gate 1ad18fa5, In Review → Ready gate 2aa6fe29):docs/architecture/explanation/rewrite-design-principles.adocviolated its own "Documentation names only live tests/code" rule by carrying unguarded live-inventory enumerations (arm lists, file censuses, occurrence counts, a dated compliance roster) that rot silently as the codebase moves, and canonized surfaces the R222/R333 pivot dissolves. New "Principles are stated at altitude" section codifies the discriminator: an inventory belongs in a principle only when a named live test pins it (theGeneratorCoverageTest-guarded dispatch partition is the kept exemplar); otherwise state the rule, one canonical exemplar, and the smell. Applied acrossSourceKey.Reader/Wraparm enumerations, the four-axisSourceKey/LoaderRegistrationcensus, two parse-boundary file censuses, theCallSiteExtractionstrategy list, thecandidateHintoccurrence census, the helper-locality compliance roster, and the R50 retired-carrier roster; vision-alignment forward notes added pointingSourceKeyat R431 andMethodBackedFieldat its R222 retirement (ServiceFieldas the current capability exemplar); staleArgCallEmitter.buildNodeIdDecodeExtraction/R260 citation replaced (R260 shipped; R334 tracks the live instance). Two review rounds found and fixed residual rot the first pass missed: gate 1 caught a still-enumerated retiredTextMapLookuppermit (R229), an 11-of-12 module census, an unguarded "thirteen resolver siblings" count, and an over-inclusivejava.lang.reflect-import discovery recipe (keyed ontoType-tree reads instead), plus same-family ride-alongs incode-generation-triggers.adoc,argument-resolution.adoc,typed-rejection.adoc,SealedHierarchyDocCoverageTest, andSourceKey.Reader’s javadoc; gate 2 caught a sentence-initial "Thirteen directive resolvers" in `typed-rejection.adocthat survived gate 1’s case-sensitive grep sweep, fixed with a case-insensitive re-sweep (zero remaining hits outside an unrelatedPGThirteentest fixture). This is a docs-only item: no generated-output or runtime change; verification is the stale-reference check on every symbol/test/roadmap-id the revised doc names, plus a clean AsciiDoctor render. Full reactor green under-Plocal-dbat each gate. -
R436 (
2992d25implementation; Backlog → Spec20b5072, Spec → Ready97c0bb3, In Progress → In Reviewe263020): Fix unsafeinto()key extraction colliding with multiset aliases and escaping error redaction. Two defects on a@service/@splitQuerysplit field over a@tableparent whose DataLoader key wrap isSourceKey.Wrap.TableRecord. Defect 1: the key read did a whole-recordenv.getSource().into(Tables.X), mapping the parent row into the typed record by column name; a sibling multiset-backed object field aliased (.as(fieldName)) to a name case-insensitively shadowing a physical column (the incident:dager/tiderover range columns; the sakila repro:Film.LengthoverFILM.LENGTHsmallint) poisoned the conversion and threw aMappingException, aggravated by R426 widening the parent SELECT to the full row. Defect 2: that throw ran synchronously in the DataFetcher body before dispatch and the async.exceptionallyrouter, so it escapedDataFetcher.get()unrouted and leaked jOOQ’s raw record-dumping message pastErrorRouterredaction (a privacy hole; per-node repetition blew the OTel gRPC 4 MiB export limit). Fix (Defect 1): the parent$fieldsprojects the full row under reservedsrc_<col>aliases (GraphQL reserves leading-for introspection, so no client-driven sibling alias can collide) andGeneratorUtils.buildKeyExtraction’s `TableRecordarm rebuilds the typed record column by column with explicit types (no runtimeField<?>loop / unchecked cast). Both emit sites drive off a new generation-timeTableRef.allColumns(populated inJooqCatalog), single-homing the projected names and the extraction’s lookup names so they cannot drift.RequiredProjectionreshaped from a sealed{ FullParentRow | Columns }sum to a product record(boolean reservedFullRow, List<ColumnRef> baseColumns); the reserved full row no longer supplies base-named columns that theWrap.Row/Wrap.Record/TableMethodFieldreads still need, so the two axes are co-present and both emitted (R426’s absorbing "type fact" javadoc rewritten). A narrow build-time validator (GraphitronSchemaValidator.validateAliasKeyColumnCollisions+parentProjectionAlias, mirroringemitSelectionSwitch) rejects a sibling alias shadowing a key/correlation column read by base name (the residual the reserved-alias fix cannot cover), with field/column/remedy in the message; the broad whole-row collision is fixed not rejected, so legitimate schemas keep working. Fix (Defect 2):DataLoaderFetcherEmitter.buildwraps extraction + dispatch + async tail intry/catch (Throwable)routing through the sameasyncRouterCalldisposition the.exceptionallyarm uses (single-homed inTypeFetcherGenerator, threaded to both arms so they cannot diverge), lifted into a completed future; the R268preRegistrationPreludestays outside the guard by design. Coverage: execution-tierGraphQLQueryTest.films_titleTitlecase_withCollidingMultisetSibling_bothResolve_noMappingException(both fields resolve post-fix), pipeline-tierServiceProjectionPipelineTesttwo-axis emit +AliasKeyColumnCollisionValidationTest(rejection + no-false-positive), unit-tierDataLoaderFetcherEmitterTestguard-shape (registration outside / extraction inside / catch-arm routes), R426 contract tests still green; no code-string assertions on generated bodies. Reservedsrc_*reaches generated code only as string literals, so the dunder lints need no allowlist entry (docs updated);handle-services.adocextraction phrasing updated. Spec-permitted fallback taken on the execution-tier redaction test (no bespoke throwing-accessor fixture): the sync catch is unit-proven, shares one router-call definition with the async arm, and the async arm’s redaction is already execution-proven viaFilm.durabilityError. Independent-session In Review → Done review; full reactor green under-Plocal-db(all 13 modules incl. thegraphitron-sakila-exampleexecution + Java-17 compile tier). Builds on R426/R425/R415/R268. -
R437 (
f293803implementation; Backlog → Spec4119cf7, Spec → Readyd8c8653, Ready → In Progress0fef51c, In Progress → In Reviewe41a4d8): Shape-awarecreate<Record>/create<Record>List@servicehelper dedup, fixing an R311/R315 silent-column-drop correctness bug. The jOOQ-TableRecord@serviceparam helpers were deduplicated by record class alone (putIfAbsent(recordClass, …)at two collection sites, and both call-site namers derived fromrecordClass.simpleName()), so two@servicefields on one type binding the same record through different input shapes (different@fieldcolumn sets) collapsed to the first-seen helper; every call site routed to that survivor and the other mutation silently wrote its unique columns as NULL/default (found infs-plattform’s `registrerCampusForUtdanningsmulighetvsdeaktivereCampusForUtdanningsmulighet, whereDATO_FRAwas dropped to1900-01-01). The fix re-keys dedup, naming, and call-site routing by the full binding shape (record class + orderedColumnBinding`s + ordered `RecordKeyDecode`s): a new `JooqRecordHelperNamesresolver dedups on the carrier’s own structuralequals(D1 ; exactly "these two emit an identical helper body", so no parallel signature function can drift), names uncontended record classes with the barecreate<Record>(byte-identical to pre-R437, no churn) and contended ones withcanonicalRender-ordered 1-based ordinal suffixes plus a one-line column-naming javadoc (D2), and is built once from every jOOQ-record carrier on the<Type>Fetchersclass (both coordinates) and stashed onTypeFetcherEmissionContextbefore any field body emits, so the helper drain and both call-site emitters (ArgCallEmitterchild,ServiceMethodCallEmitterroot incl. the list arm) resolve the same name by construction (D3); a populated resolver throws on an uncollected carrier rather than silently falling back to a bare name (routing-hole tripwire), while the defaultbare()resolver preserves today’s behaviour for schema-free/unit/out-of-band contexts. Accepted limitation: collapse compares bindings in producer order, so identical columns in different SDL declaration order would not collapse (a missed collapse, not a correctness bug). Coverage: pipeline-tierJooqRecordServiceParamPipelineTestR437 group ; contended-singular red regression (two distinct helpers, each fetcher routes to its own, exactly one setsRELEASE_YEAR), contended-list arm pinned separately, cross-input-type collapse to one bare helper (pins shape-keying over input-type-name keying), determinism across runs, and contended-vs-uncontended javadoc; the R311/R315/R322/R336 pins keep passing (36/36). Independent-session In Review → Done review; full reactor green under-Plocal-dbincl. thegraphitron-sakila-exampleJava-17 compile (no generated-output churn). Builds on R311/R315. -
R410 (
e7ae955slice 1,1fae7e9slice 2,55e04e3slice 3,6149cfeslice 4,a0e6950slice 5,d77fd0cslice 6; review fixes in the In Review → Done commit range):graphitron:devowns incremental compilation of generated sources. The dev loop now turns generated.javainto.classin-process, into the graphitron-exclusivetarget/graphitron-classes(sole-writer dir, first-on-classpath precedence for consumers that load external.class;quarkus:devempirically confirmed not to consume it, so the Quarkus value routes to the in-process MCP query-execution driver, filed as R428/R429). Composition: the idempotent writer reports its per-run delta (slice 1);CompileDependencyGraphis projected from the classified model through exhaustive switches over theGraphitronType/GraphitronFieldleaves mirroringprojectFieldClassification, with frozen-vs-growingUtilSingletonclassification so blanket over-approximation never harms pruning (slice 2);AbiSignaturehashes the signature surface (constant values included for javac inlining; type variables with bounds added in review) andRecompileSetcomputesdelta ∪ ABI-changed reverse-transitive dependentsas pure functions (slice 3); a warmJavaCompiler/StandardJavaFileManagerengine with per-round fresh tasks, orphan.classsweep, and a dedicatedCompileDiagnosticchannel kept separate fromValidationReport(slice 4); the two-clause acceptance harness (incremental tree byte-for-byte equals clean full compile; body-edit prunes / ABI-edit propagates) plus theTypeSpecReferenceWalkcompleteness oracle (slice 5);DevMojowiring with-Dgraphitron.dev.compile=falseopt-out, no fail-fast (exclusive dir degrades safely), conservative whole-tree recompile on consumer.classchange, and compile diagnostics surfacing through the console block (CompileErrorFormatter) and the MCPdiagnosticstool with asource: "schema"|"compile"discriminator (slice 6). Independent-session In Review → Done review landed four fixes: the completeness oracle widened with a@nodetype, which falsified missing node-lookup wiring edges (QueryNodeFetcher/EntityFetcherDispatchincluding the per-node-typetypes.<T>projection references andentitiesByTypefederation targets) ; closed model-sourced in the builder; failed compile rounds now carry a retry set so an unrelated save can never report clean while a stale last-good.classlingers; a first recompile with no ABI baseline (skipInitial) establishes the full image instead of a half-populated dir;AbiSignaturegained type-variable/bounds coverage andDevMojo.lastGenerationbecame volatile (cross-watcher-thread visibility). The spec’s third diagnostics channel (LSP publish against generated-file URIs, best-effort) did not ship and is filed as R430. Residuals accepted per spec: no live graph-completeness guard (offline oracle only); generated→consumer invalidation is conservative whole-tree pending R333’s method graph, where the sourcing seam re-targets its live exhaustive switches. Full reactor green under-Plocal-db. -
R426 (
3931908implementation; Backlog → Specccbaea5, Spec → Readyb774532, In Progress → In Review0d01fc1): Project the full parent row for TableRecord-sourced@servicechildren, honoring the already-documented contract of the typed-TableRecordsource shape. A@servicechild whoseSourcesparameter is a typedTableRecord(Set<FilmRecord>,SourceKey.Wrap.TableRecord) receives keys viaenv.getSource().into(Tables.X), but the parent$fieldsSELECT projected only the client’s selection plus R425’s force-included key columns, so a service body reading a non-key column (FilmService.titleTitlecase’s `film.getTitle()) got a silent null whenever the client didn’t happen to select that column; the in-tree execution test passed only because its query selectedtitlealongside, and the federation_entitiesshape (router selects just the service child) hit the failure in production form. The manual (handle-services.adoc) already promised "fully-populated parent records (every column on the parent table)", so the fix makes the codegen honor the written contract rather than rewriting it. Implementation folds R425’s key-column collection and the new full-row signal into one walk (TypeClassGenerator.collectRequiredProjection) returning a sealedRequiredProjection { FullParentRow | Columns }with an absorbing combine (FullParentRowdominates; "full row subsumes columns" is a type fact, not a dedup accident);build$FieldsMethodswitches once, the full-row arm emitting a singleCollections.addAll(fields, table.fields())append that is alias-correct by construction (the caller’stableparam carries base column names, the same namesinto(Tables.X)reads by). Gated on the key wrap (SourceKey.Wrap.TableRecord), not the sealed field variants, so any futureBatchKeyFieldacquiring the wrap gets the right projection for free; sits after R425’s record-parent guard so only table-parent fields reach it. Coverage: pipeline-tierServiceProjectionPipelineTestR426 group (full-row append forServiceTableFieldandServiceRecordFieldTableRecord-sourced children via the newTypeSpecAssertions.appendsFullParentRowhelper, aRecord1-sourced contrast sibling pinning the wrap gating, and aNestingFieldrecursion case); execution-tierGraphQLQueryTest.films_titleTitlecase_withoutSelectingTitle_readsNonKeyColumnOffSourceRecordunmasks the in-tree reproducer ({ films { titleTitlecase } }with notitleselected, red pre-fix); federation execution-tierFederationEntitiesDispatchTest.entities_tableRecordServiceChildOnly_nonKeyColumnReadResolvesNonNull(representations-driven fetch, non-key read). Docs:handle-services.adoccontract prose stated plainly, projection-cost note added, and the caveat paragraph’s false table-parent half reconciled. Known residual imprecision noted at review: the full-row append is unconditional in$fields(fires on every fetch of a parent type carrying such a child, selected or not), so the docs' "whenever the field is selected" cost framing understates the trigger; this matches R425’s unconditional force-include shape, and selection-gating the required projection is a possible future refinement. Builds on R425. Independent-session In Review → Done review; full reactor green under-Plocal-db. -
R424 (
7400c67core,1a855a4rework; Backlog → Spec889f6cb, Spec → Readyc205824, In Review → Ready rework814e255, In Progress → In Reviewa5c485f): Route inline (non-@splitQuery)@referencefield argument reads through the field’s ownSelectedFieldinstead of the ancestor fetcher’s env. Inside the generated<Type>.$fields(sel, table, env)method,envbelongs to the top-level operation fetcher, soenv.getArgument("filter")returned null, the filter condition collapsed tonoCondition(), and the field silently returned unfiltered rows (data-correctness bug, discovered via an opptak-subgraph reproducer where a@nodeIdfilter onStudiekurv.kladderwas ignored; the@splitQuerysibling behaved because its env genuinely is the field’s own). Fix threads a sealedArgumentValueSource(Env|FromSelectedField(sfLocal)) throughFkTargetConditionEmitter.emitTerm→ArgCallEmitter.buildCallArgs→buildArgExtraction; root/split sites passEnv(byte-identical output), the two inline emitters passFromSelectedField(sfName)so runtime reads resolvesf.getArguments().get(name). Covers the filter-condition path, the inlinefirstpagination limit, and theJooqConvert+list pre-lift (emitJooqConvertKeyLifts, added to both inline emitters by parity ; pre-R424 that inline shape emitted a reference to an undeclared<name>Keyslocal; the helper takesFromSelectedFielddirectly since it has noEnvcaller).ContextArgstays env-based (request-scoped context is legitimately the ancestor env); never-inline arms (InputBean/JooqRecord) guard withIllegalStateException. The$fieldshost stamps@SuppressWarnings("unchecked")via the source-awareCallParam.emitsUncheckedCastFromSelectedField, keeping theEnvhosts' warning-free output unchanged. Coverage: pipeline-tierInlineFilterArgumentSourcePipelineTest(source-aware suppression stamp incl. the top-levelJooqConvert+list pin over the non-@nodestore → customerFK, scalar negative case, end-to-end generation; annotation/model assertions only), execution-tierGraphQLQueryTestagainst real PostgreSQL (Store.customersByFirstName{,Split}narrowing + inline/split parity,customersFirstNlimit, and the decode-consumingCustomerByNodeIdFilterfixture where a foreign store’s node id narrows the inline child list to empty with a@splitQueryparity mirror); compilation tier via the sakila-example-Werrorbuild. Independent-session In Review → Done review; full reactor green under-Plocal-db. -
R425 (
9c2c9abimplementation; Backlog → Spec119bfb2, Spec → Ready2969866, In Progress → In Review70484cd): Force-include a@service/@splitQuerychild’sSourceKeycolumns in the parent$fieldsprojection so its DataLoader key is never silently null. A@splitQuerychild builds its DataLoader key off the parent source record (Record) env.getSource(.into(<ParentTable>)), but the parent SELECT is driven purely by the client’s GraphQL selection set, so when the client selected the child without selecting a field mapping to the key column, the column was absent from the parent row, key extraction read null, and the child resolved to null with no error, biting hardest under federation where an Apollo Router_entitiesfetch supplies keys viarepresentationswithout re-selecting them. Root cause was a pattern-match omission:TypeClassGenerator.collectRequiredProjectionColumnsmatched the twoSplit*arms (so split-@referencechildren were already covered) but letServiceTableField/ServiceRecordFieldfall through toStream.empty(). Fixed by collapsing the twoSplit*arms into a singleBatchKeyFieldcapability arm returningsourceKey().columns()(the capability-interface case per the capability-vs-sealed-switch principle; the enumeration already had a blind spot, its javadoc listed six implementers where seven exist). The three record-parent implementers (RecordTableField,RecordLookupTableField,RecordTableMethodField) are guarded with a loudIllegalStateExceptionrather than routed: they key off a Java accessor viabuildRecordParentKeyExtractionand can carry target-aligned columns, so a leak into the table-parent walk would silently project wrong columns, the same silent-null family this item fixes; the guard fails at generation time instead. One refinement beyond the literal spec: a nullSourceKey(a@servicemethod taking no Sources param, a plain per-parent delegation with no key read) contributes no columns rather than NPEing. Fold-ins:BatchKeyFieldjavadoc gains the missingRecordTableMethodField; the two force-include taxonomy comments name the capability instead of enumeratingSplit*. Coverage: pipeline-tierServiceProjectionPipelineTest(table-bound returnServiceTableField, scalar returnServiceRecordField, and a service child nested under aNestingField, each on a parent with no other force-projecting child, assertingTypeSpecAssertions.appendsRequiredColumnon the parent PK); execution-tierGraphQLQueryTeston a new unmaskedCityfixture (no@splitQuery/@tableMethodsibling, so the@servicechildren are the only reasonCITY_IDis projected) coveringcityUppercase(Wrap.TableRecord, the silent-null shape) andcityLowercase(Wrap.Row, the loud-throw shape) queried without any key-mapped field, backed by the newCityService; federation execution-tierFederationEntitiesDispatchTestwith a representations-driven_entitiesfetch selecting only the service child (the opptak reproducer shape,Citypromoted to a@keyentity,FederationBuildSmokeTest’s `_Entityunion pin updated); plus a note on the existingFilmservice-child tests thatcast/castByKeymask this behaviour forFilm. No code-string assertions on generated method bodies (pipeline tier uses the spec-namedappendsRequiredColumnshape helper). Developer-sideWrap.TableRecordcontract hazard filed separately as R426; distinct from R424 (child arguments read from the wrongenv). Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db. -
R421 (
fb4ca33guard + tests + pom + seam;8337c14test references the seam’sFAULT_HEADERconstant;44fc1b0move fault seam to a test@Alternative+ pin the true single contract; Specbe489aa/514e2f0, Spec → Ready9927eb8, In Progress → In Review83d792a, spec wording correction6ac9f29): StopGraphqlResource.execute()ingraphitron-jakarta-restfrom leaking internals when the server-side execution path throws. The resource shaped every request error (400/405/422) into a spec-compliantapplication/graphql-response+jsonbody but ran the consumer-implemented, auth-seededapplication.newExecutionInput()SPI seam andengine.execute()unguarded; a fault there (observed: the seam forcing a JDBC connection with the DB down, aCreationExceptionwrappingPSQLException) escaped past every spec-shaping branch into the container’s generic error handler, dumping the exception chain, stack, DB host/port, and internal package names as a non-spec response. Fixed with an ordered two-arm catch around both calls:catch (WebApplicationException)re-throws unredacted so a consumer signalling a client-facing 4xx from its adapter (e.g.ForbiddenException→ 403) has JAX-RS map the intended status rather than collapsing it to a redacted 500 (arm order is load-bearing:WebApplicationExceptionis aRuntimeException, so the broad arm would otherwise swallow it);catch (Exception)mints aUUIDcorrelation id, logs the real cause server-side via SLF4J, and returns HTTP 500 (modern) / 200 (legacy) carrying the reference-only wire shape{errors:[{message:"An error occurred. Reference: <uuid>."}]}with no extensions, byte-identical to the message the generatedErrorRouter.redactemits (ErrorRouterClassGeneratorline 481). This resource-level guard is the structural complement toErrorRouter’s per-fetcher redaction: `newExecutionInput()runs before graphql-java execution begins, the one region neitherErrorRouternor graphql-java’s own handling can see. The single-contract claim is the reference message, not byte-identity of the whole error object: the fetcher path builds through graphql-java’sGraphqlErrorBuilderwhich serialises a defaultextensions.classification, while the resource emits a plain{message}with no extensions, consistent with its own 400/422 errors; matching graphql-java’s classification from the resource was rejected (would contradict the no-extensions requirement and misclassify a pre-execution input-building fault as a data-fetching one). Vendor-neutral: the only dependency added to the module pom isorg.slf4j:slf4j-apiatprovidedscope (already version-pinned in the parentdependencyManagement);jakarta.ws.rs.WebApplicationExceptionrides the existingprovidedjakarta.ws.rs-api, and no RESTEasy/Quarkus type is named. Testing lives ingraphitron-sakila-example’s `GraphQLOverHttpConformanceTest(R399: thegraphitron-jakarta-restmodule carries no@Testclasses of its own): four R421 cases (redacted 500 modern with no leaked internals, redacted 200 legacy,WebApplicationException→ 403 passthrough, andredactionShapeMatchesFetcherPathpinning message-identity on both legs / no-extensions on the resource leg / classification-present-but-clean on the fetcher leg via the existingFilm.durabilityError@serviceleaf). Fault injection is a test-scoped@Alternative @Prioritybean,FaultInjectingGraphitronApplication, which subclasses the real adapter and throws on a sentinelX-Graphitron-Faultheader while delegating every other request tosuper.newExecutionInput(), soexecute()still drives the real seam wiring end-to-end and the shipped reference adapter (SakilaGraphitronApplication, a subgraph-author template) stays pristine. No code-string assertions on generated bodies. The trace-correlation follow-up (redaction reference id derived from an OTeltrace_idvia MDC, rather than a fresh UUID) was deliberately deferred to Backlog R423. Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db(GraphQLOverHttpConformanceTest15/15, 0 failures across the reactor). -
R408 (
352b05bcimplementation, option A; In Progress → In Review599cbc28, Spec → Readya3564f2a): Give consumers a build-side lint-finding suppression mechanism, the suppression half of the configurability follow-on R398 deferred. A<lint>block on the Maven plugin config carries two axes with deliberately different scope:<disabledRules>names rule ids to silence everywhere, and<excludedTypes>names type-name globs (any run,?one char) to skip in the SDL lint engine’s AST walk. Option A (project config, whole-rule + type-name-pattern disable) was chosen at Ready sign-off over the node-local@lintDisabledirective (option B, deferred until a consumer hits granularity a name pattern cannot express) and inline SDL comments (option C, rejected for fragile graphql-java comment-to-node anchoring).LintBindingcollapses the POM block into aLintConfig(Set<String> disabledRuleIds, List<String> excludedTypePatterns)record onRewriteContext;LintConfig.validatedtypes each disabled id againstLintRule.ids()and fails the build (viaMojoExecutionException) naming the offending id(s) and listing the valid namespace, so a typo is a build error not a silently-ignored line. Suppression is applied at the one build evaluator (GraphQLRewriteGenerator.withLintFindings), not in a Maven-log-only filter: the disabled-rule filter runs over the *combinedBuildWarninglist after classifier advisories (schema.warnings()) and engine findings are concatenated, so it keys on the typed rule id and covers both channels (aSource.CLASSIFIERadvisory likesplitquery-redundant-on-record-parentis suppressible by id like any engine rule); the type-name-glob filter runs insideLintEngine.run, widening the same per-type skip boundary the bundled-type and R407 federation-injected exclusions use, and stays scoped to the engine’s AST walk. That asymmetry is deliberate and pinned by test: the classifier advisories arrive pre-formed with no structured owning-type handle to glob against, so a classifier advisory on anexcludedTypes-matched type still fires (reverse-mapping a location or scraping the type name out of message text is the fragile-anchor trap option C rejects). Because the LSP replays theValidationReportand the MCPdiagnosticstool projects it, andDevMojobuilds its context through the samebuildLintConfigseam, a suppressed finding never surfaces in CI, the editor squiggle, or the MCP tool, from one definition with no second filter. Non-goals held: no severity overrides / error-capable lint (everything stays a warning), no@lintDisabledirective, no plugin SPI for new rules. Coverage: pipeline-tierLintSuppressionPipelineTest(5: disabled-rule drops that rule while others fire,excludedTypesskips the matching type but not siblings, glob match, classifier-advisory-by-id suppression, and the engine-scopedexcludedTypesasymmetry), unit-tierLintConfigTest(unknown-id validation failure listing the valid namespace, both-axes build, empty config), and single-evaluator parity tests at the LSP tier (LintSuppressionDiagnosticsParityTest: a build-suppressed finding does not replay as a squiggle while a co-present rule still does) and MCP tier (same, through the livediagnosticstool); assertions key on the typedLintRule/lintRulewire field, no code-string assertions on generated bodies. User docs shipped to their real home (docs/manual/reference/mojo-configuration.adoc, the<lint>parameter row plus a "Silencing lint warnings" section). Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db. Builds on R398 (build-is-single-evaluator spine, typedLintRuleid); the "author chooses not to fix" sibling of R407’s "cannot fix" exclusion, landing at the sameLintEngine.runboundary. -
R396 (
2509653implementation; In Progress → In Reviewe79a746, site-6 Spec review0aeb581, Spec revise7082fa5, Backlog → Spec5ff535b): Accept schema-qualified or case-mismatched@table(name:)base names on the@referenceFK-connection-and-orientation path. A type declared@table(name: "multischema_a.signal")(ormultischema_a.SIGNALover the real lowercase name) could not attach an@reference(path: [{key: "<fk>"}])field: the verbatim, case-preserved@tableecho was compared by bareequalsIgnoreCaseagainst jOOQ’s always-unqualified FK endpoint names, so the FK read as "does not connect" (Author error: key '<fk>' does not connect to table '<name>'), and where a partial fix let it through, the same bare compare in the orientation predicate silently mis-oriented the join (origin/target swapped, slot pairing inverted). Reported against 10.0.0-RC21 (opptak), the@referencesibling of R395. Fixed by identity comparison, not input sanitization: two newJooqCatalogprimitives,foreignKeyTouchesTable(source-side membership, either endpoint) andforeignKeyOnSource(orientation; self-referential FKs fall to the caller’sselfRefHint), resolve the source through the schema-awarefindTableand compare FK endpoints by jOOQ table class identity (endpoint.getClass() == resolvedSource.getClass()), falling back to the historical bare compare when the source isAmbiguous/NotInCatalogso the diagnostic surface for genuinely-unknown names is unchanged; class identity also distinguishes same-named tables across schemas, which a normalized bare-name compare cannot. All six spec-enumerated sites routed through the primitives: Phase 1 ({key:} path)parsePathElementconnection check →foreignKeyTouchesTable; orientation decided once viaforeignKeyOnSourceinsynthesizeFkJoinand threaded intoresolveFkSlotsas a precomputedboolean fkOnSource(signature change), so the FK-orientation predicate lives in exactly one place;resolveRecordFkTargetColumns(site 5) uses the primitive for both its implicit-inference directional filter and slot orientation. Phase 2 ({table:} + empty inference)findForeignKeysBetweenTablesresolves each argument to class identity;findUniqueFkToTable(site 4) andqualifierForFk(site 6, the synthesis-shim path whose bare re-filter turned a qualified-@tableinput type into a hardIllegalStateException) re-filter throughforeignKeyOnSource. One documented deviation, endorsed on review: site 6 usesforeignKeyOnSource(…, selfRefHint=true)rather than the spec’s suggestedforeignKeyTouchesTable, preserving the method’s strictly source-side semantics and the existingqualifierForFk_wrongSourceTable_returnsEmptytest. The verbatim echo stays the source name everywhere, so author-error diagnostics still quote what the user wrote; only the comparison changed. Phase 3 (qualified return-type terminal verdict viaTableRef.sameTable) split out to R422 per the spec’s own scope recommendation. Coverage: unit-tierJooqCatalogMultiSchemaTest(both primitives oversignal_widget_id_fkeywith qualified and upper-case sources, referenced-side/non-endpoint/cross-schema-same-name cases, qualified-both-argsfindForeignKeysBetweenTables, and asynthesizeFkJoinqualified-source orientation guard pinning origin=signal/target=widget and slot orientation); pipeline-tierQualifiedSourceReferencePipelineTest(all three@referenceforms plus the qualified-and-upper-case spelling classify to a correctly-orientedFkJoin, no author error); execution-tierMultiSchemaQueryTestwith the R395 fixture tightened from@table(name: "signal")to its originally-specified@table(name: "multischema_a.SIGNAL"), rows still routing to the discriminated types andAlertSignal.widgetNamepopulating through the now-validated cross-table@reference, with R395’s discriminator-qualifier coverage preserved (FROM still renders"multischema_a"."signal"). Known residual, non-blocking:foreignKeysTouchingTable(candidate-hint scoping on the error path) keeps the bare compare, thinning the "did you mean" list for a qualified source without affecting any verdict. Builds on R395 (dependency honored: R395 Done atf6cc9aebefore R396 entered In Progress at3518bb9); spawns R422. Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db(unit 55, pipeline 4, execution 4 in the touched suites; 0 failures across the reactor). -
R418 (
8213374implementation; In Progress → In Review2eb9752, Spec → Ready8d80d72, Backlog → Spec440b207, Backlog146d334): Make the web-sandbox SessionStart hook drop-and-recreaterewrite_teston every start, so the fixture DB is a pure function of the checked-outinit.sql. Step 2 of.claude/scripts/session-start-web-env.shpreviously created + seededrewrite_testonly when the database was absent; a sandbox first seeded from an olderinit.sql(e.g. one predating R389’sparty_*joined-table fixtures + thejti_*composite PKs) kept that stale schema forever, and-Plocal-dbjOOQ codegen then built its catalog against the stale DB, cascading into theUnclassifiedType/UnclassifiedFieldfailures (Query.allParties,JoinedTableInheritancePipelineTest, and siblings) that every recent Done review documented as a manual re-seed. The existence guard is replaced by an unconditionalDROP DATABASE IF EXISTS rewrite_test WITH (FORCE)(PG13+, cluster is PG16, so a lingering backend from a prior session cannot block the drop) +CREATE DATABASE+ reseed, still inside the unchangedpg_ctlcluster/pg_isreadygate so local dev (TestContainers, no persistentrewrite_test) stays a no-op; the password-reset line and itspg_was_runningguard are untouched, and no checksum/skip optimization is added (sub-second reseed, and a skip-guard would reintroduce the staleness window)..claude/web-environment.mdupdated: the "Brings up PostgreSQL" step-2 bullet now states the drop/recreate/reseed-every-session behavior, and the Catalog-jar clobber section gains a note that a stale sandbox DB was a second, now-eliminated cause of the same cascade so a future reader does not misfile a DB-staleness failure as a catalog-jar clobber. Web-sandbox tooling only, no reactor/generator/test orinit.sqlchange; the R389jooq.codegen.schema.versionbump is orthogonal and untouched. Verification is manual shell testing (spec § Verification):bash -nparses; the hook run against the live PG16 cluster fully replaces a stale DB (party present, 3 rows; a planted stale marker gone), force-terminates a lingering backend, and two back-to-back runs both succeed. Independent-session In Review → Done review; on the reviewer sandbox the reseeded catalog resolved the party corpus (JoinedTableInheritancePipelineTest5/5,VariantCoverageTest3/3 green under-Plocal-db). This item is the standing fix R413/R414/R384/R415/R407/R182 named for the recurring stale-rewrite_testreview artifact. -
R413 (
0d4a2d3implementation; In Progress → In Review93e57a0, Spec → Readyd4f7401, Backlog → Specdc18b15): Bind the parent-inputVALUEScells of split/reference DataLoader rows methods through the key column’s jOOQ ConverterDataType. The rows methods built each parent-key cell straight from the raw keyField(typed byColumnRef.columnClass(), the converted user type), so a converter-backed or domain-typed key column rendered at the wrong SQL type and the correlation JOIN had no matching operator; against the utdanningsregisteret consumer schema,Campus.ORGANISASJONSKODE(kodeverk.kode_numerisk_domainover BIGINT with aConverter<Long, String>) bound ascharacter varyingand every@splitQuery/@referencechild nulled out withoperator does not exist: kodeverk.kode_numerisk_domain = character varying, invisible on the plain-typed Sakila keys. Fixed at the VALUES-emission seam (the one choke point every key passes through), not at key construction: all four parent-input sites,SplitRowsMethodEmitter.emitParentInputAndFkChain(list/single/connection prelude) +emitFromBridgeAndParentJoin,emitRecordTableMethodBody(@tableMethodvariant),buildServiceTableLift(R285 lift-back re-projection), andMultiTablePolymorphicEmitter.buildParentInputValuesEmitter+ per-branch ON lookups, now route their cells throughValuesJoinRowBuilder.cellsCode(extended with a constants-class-table-expression variant, making its single-VALUES-cell-authority javadoc claim true), emittingDSL.val(<scalar>, Tables.<OWNER>.<COL>.getDataType())so jOOQ binds through the registered Converter at the DB type (coerceandcastwere verified against jOOQ 3.20.11 and rejected in the Spec). The scalar extraction forks onSourceKey.Wrap, the axis that actually decides value accessors (replacing the coupledreader() instanceof AccessorCallfork):RecordNkeys readk.valueN();RowNkeys recover the value from the bindParamvia a new per-fetcher-classparentKeyCellValuehelper with a loud statement-body throw, the documented contract for@sourceRowlifter keys (unenforceable at validate time, so pinned live at the execution tier). The owner table is a model fact:ParentCorrelation.parentKeyOwnerTable()folds the three-arm fork once (FkJoinorigin /LiftedHoptarget /ConditionJoinparent); the polymorphic arm’s owner (the parent/hub table) ridesInterfaceField/UnionFieldas a non-nullparentKeyOwnerTablecomponent threaded from the resolution sites. JOIN-predicateparentInput.field(…)lookups switch to the owner column’sDataTypefor symmetric type metadata;RowN/RecordNgeneric type-args staycolumnClass()-typed (the converter’s user type IS the Java-side type). Fixtures pin end-to-end:org_code_domain(BIGINT) +converter_org/converter_campusin the fixture DB (schema version 2.3→2.4),OrgCodeStringConverteringraphitron-fixtures-codegenregistered via<forcedTypes>, and sakila-example execution tests covering single-cardinality@splitQuery(the reportedCampus.organisasjonshape), list-cardinality@splitQuery, and a@sourceRowlifter over the converter-backed key (the liveparentKeyCellValueParam-contract pin); existing unit expectations updated in place, no new body-substring assertions. Independent-session In Review → Done review; full reactor green undermvn clean install -Plocal-db(2419 core tests + sakila-example compilation/execution tiers,GraphQLQueryTest297 tests, 0 failures) after re-seeding a stale localrewrite_testDB (missing the R389partyfixtures and R413’s ownconverter_*tables), the sameQuery.allParties/JoinedTableInheritancePipelineTeststale-DB artifact every recent Done review documents (standing fix: R418), on paths R413 does not touch. -
R414 (
f106d52implementation; In Progress → In Reviewae9226b, Spec → Readyc0b4ff7, Backlog → Spec6ea032b): Serve a real per-parenttotalCounton split/DataLoader-backed connections, closing the last classification path that produced null-(table, condition)ConnectionResultcarriers on reachable queries. A nested (non-root)@splitQueryconnection advertisedtotalCount: Intin the emitted SDL but always resolved it tonull: the scatter path (SplitRowsMethodEmitter.scatterConnectionByIdx) built each per-parentConnectionResultthrough the(result, page)convenience constructor, which passednullfor the(table, condition)pair the generatedConnectionHelper.totalCountneeds to issue itsSELECT count(). The fix mirrors the B4c-2 polymorphic-batched semantics (MultiTablePolymorphicEmitter.buildBatchedConnectionRowsMethod): the rows method hoists its WHERE into a singleCondition wherelocal (buildWhereConditioncalled exactly once, since it declares FK-target alias locals as a side effect) shared by the windowed page query and a new cursor-independentcountSourcederived table (same join topology viaemitFromBridgeAndParentJoin, no orderBy/seek so the count is window-independent);scatterConnectionByIdxgains aTable<?> countSourceparameter and binds each per-parent carrier via the 4-argConnectionResultconstructor withcountSource.field("idx", Integer.class).eq(DSL.inline(i)), soConnectionHelper.totalCountrunsSELECT count() FROM countSource WHERE idx = ilazily on selection (zero count SQL when unselected, N counts for a batch of N parents when selected). The now-dead two-arg(result, page)ConnectionResultconstructor is removed (scatterConnectionByIdxwas its only caller; this is generated-into-consumer source, not published API); the nullable(table, condition)field shape and theif (cr.table() == null || cr.condition() == null) return nullguard stay for the one validator-unreachable producer that remains,MultiTablePolymorphicEmitter.buildRootConnectionFetcher’s defensive empty-participants `new ConnectionResult(List.of(), page, null, null), and the three "Split-Connection scatter passes null" comments are re-pointed to name that remaining producer rather than narrowed to non-null. Sakila’sActorsConnection(shared byFilm.actorsConnection+Film.actorsOrderedConnection, both@splitQuery) gainstotalCount: Int. True B4c-2 structural unification (a shared materialised pre-window derived table feeding both the ranked window and the count) is a deliberate non-goal: the split page query’s.orderBy(page.effectiveOrderBy()).seek()reference live terminal-alias columns, and re-pointing them at derived-table fields would rework well-tested pagination for no user-visible gain; the dual topology emission shares oneemitFromBridgeAndParentJoinhelper and the single hoistedwhere, so drift risk is low (possible follow-up if it ever drifts). Coverage: execution-tierGraphQLQueryTest(splitQueryConnection_totalCount_isParentScopedcounts 2/2/1/1/1 for films 1-5 against the seededfilm_actorrows withfirst: 1to distinguish the count from the page size;isCursorIndependentstill reports 2 paging past anaftercursor;_isLazyOnSelectionasserts noselect countSQL when unselected and exactly N per-parent count statements when selected) and pipeline-tierSplitTableFieldPipelineTest(structural:scatterConnectionByIdxcarries theTable<?> countSourceparameter), no code-string assertions on generated method bodies. Mirrors B4c-2’s count semantics; per-parenttotalCountfor hypothetical future split shapes (SplitLookupTableFieldhas no connection arm today) stays out of scope. Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db(2418 core tests + the sakila-example compilation/execution tiers, 0 failures). Thegraphitron-core red first seen in the review sandbox (Query.allParties→UnclassifiedField;JoinedTableInheritancePipelineTestClassCast on the R389party/jti*fixtures) was the known stale-rewrite_test-DB artifact every recent Done review documents, the local DB predating the R389 fixtures and jOOQ’sschemaVersionProvidersuppressing catalog regeneration; re-seeding frominit.sql+ a clean catalog regen produced a fully green reactor including R414’s execution tier, on paths R414 does not touch (the R418 always-reseed hook is the standing fix). -
R384 (
60b58aephase 0 plumbing,6021bdaphase aJooqConvert,fb1b2e4phase bNodeIdDecodeKeys,feadbcbphase c developer@condition,24b2527In Progress → In Review; Spec → Ready re-reviewbd2e761): Lift the remaining filter-argument kinds onto multitable interface/union root query fields, completing the arc R363 (branch-safeDirect/EnumValueOf/ContextArg) and R383 (nested-input@field) opened. Where R383 needed zero new plumbing, these three kinds share it, so the work is one multi-phase item over a single seam: phase 0 widensMultiTablePolymorphicEmitter’s root entry points to carry the enclosing `<Type>Fetchersclass’sCompositeDecodeHelperRegistryand pre-declares (as statements ahead of the inline stage-1 union) the locals branch filter terms cannot introduce themselves, FK-target join-hop aliases (FkTargetConditionEmitter.declareAliasesper participant, namespaced by thestage1_<Type>base), deduped<name>Keyslocals forJooqConvert-list args, and shared lifted-outerMaplocals, replacingbranchFilterWhere’s former `emitTerm(…, null, null, Map.of())with the threaded values (behaviorally inert, proving the seam before any arm flips). Phase a flips theJooqConvertarm branch-safe and, per R267 (fix a deprecation-for-removal at the source, never suppress), replaces the deprecated-for-removalDataType.convert(Object)inArgCallEmitter’s shared arm with the non-deprecated `DSL.val(raw, col.getDataType()).getValue()coercion (the design-principles §"Column value binding" idiom,.equals-identical to the deprecated form on a converted domain type, verified against jOOQ 3.20.11), correcting the single-table path in the same change; it also aligns the nested@fieldleaf with top-level conversion semantics (an ID-typed nested@fieldover a plain column now carries aJooqConvertleaf instead of the hardcodedDirect) and carvesJooqConvertleaves out ofCallParam.emitsUncheckedCast(theinstanceof List<?>guard casts nothing). Phase b flipsNodeIdDecodeKeysbranch-safe, homing the drained decode helpers on the<Type>Fetchersclass hosting the branch call site (a documented revision of the Ready text’s per-participant-composer wording, which put wire-decode machinery on the env-free pure-function composer and was not implementable; the per-classcollectIntobracket is the true single-table precedent). Phase c removes the R363 field/arg-levelhasConditionpre-guard and relaxesfirstUnsupportedFilterArg’s first guard so developer `@conditionfilters (ConditionFilter/FkTargetConditionFilter) gate uniformly on their per-param extractions; the developer method reflects once per participant and runs against each branch’s stage-1 alias, aTable<?>first parameter serving every branch while a concrete-table parameter surfaces a mismatch at consumer javac (R379 semantics). The exhaustive nine-permitisBranchSafeExtractionswitch stays the forcing function:NodeIdDecodeRecord/InputBean/JooqRecordremain explicitfalsearms (mutation-input/record-decode shapes that do not occur as a multitable root-query filter arg). Coverage: pipeline-tierMultiTableFilterLoweringTest(each kind’s rejection test flips to a lowered per-participant assertion on the model,JooqConvert/NodeIdDecodeKeysextraction, nested-leaf alignment, three@conditioncases including nested-input) and execution-tierMultiTableFilterExecutionTestover theAddressOccupant = Customer | Staffunion (occupantsByStoreIdper-branch coercion,occupantsByAddressdecode-and-filter plus a wrong-type-id client-error,occupantsStartingWithM/occupantsByNamePrefix/OccupantFilter.namePrefixvia the newTable<?>-genericMultiTableConditionFixtures), withgraphitron-sakila-example’s `-Xlint:all -Werrorcompile the pin that phase a stays off the deprecated form; no code-string assertions on generated bodies. Independent-session In Review → Done review; full reactor green undermvn clean install -Plocal-db(0 failures) after re-seeding a stale localrewrite_testDB (missing the R389partyfixture) and regenerating the jOOQ catalog, the sameQuery.allParties/JoinedTableInheritancePipelineTeststale-DB artifact earlier Done reviews noted, on paths R384 does not touch. Builds on R363 / R383; pins jOOQ coercion with R267 / R379. -
R415 (
4be03d6; Spec → Ready197307d, Backlog → Spec51503c5): Clamp connectionfirst/lastat the single runtime choke point and unify the no-channel error disposition. A negativefirst/laston a connection field flowed unvalidated into the SQLLIMIT, so PostgreSQL threwLIMIT must not be negative, which the framework redacted into an opaque correlation-id 500 instead of a client-facing validation error;first: 2147483647produced the identical redacted 500 vialimit = pageSize + 1wrapping toInteger.MIN_VALUE. Fixed in the emittedConnectionHelper.pageRequest(the one choke point every connection flavour funnels through): three guards next to the existing mutual-exclusion check, all throwing the R378 client-error markerGraphitronClientExceptionso the real message reaches the client, negativefirst, negativelast, and the derived-limit overflow (pageSize == Integer.MAX_VALUE, guarding the value PostgreSQL actually enforces rather than each input, which also covers a pathologicaldefaultPageSize); the pre-existing mutual-exclusionIllegalArgumentException("first and last must not both be specified")migrated onto the same marker (same client-mistake family, same redaction defect).first: 0stays valid. The purity note reworded to the load-bearing property (pageRequesttakes noDataFetchingEnvironment; the marker subclassesGraphqlErrorException, so "no graphql-java dependency" was never the true invariant) at bothConnectionHelperClassGeneratorandTypeFetcherGenerator:4792. Second defect, drift R378 introduced: the no-channel disposition is one decision at four emit sites across two emitters, and R378 flipped only the two sync catch arms tosurfaceClientErrorOrRedactwhile leaving the two async.exceptionallyarms on plainredact, so a client error on a nested (DataLoader-based)@splitQueryconnection would still redact. Lifted the router call into one shared definitionErrorRouterClassGenerator.noChannelRouterCall(outputPackage, throwableVar)consulted by all no-channel sites, bothnoChannelCatchArm`s, both `asyncWrapTailno-channel branches, andChannelCatchArmEmitter(a fifth site already on the right disposition); the two async arms flip tosurfaceClientErrorOrRedact, whose cause-chain walk unwraps theCompletionExceptionDataLoader wraps around a batch-function throw, so the marker surfaces while everything else keeps redacting (blast radius bounded to the marker type). The next disposition change is now one edit, not a four-site hand-coordination. Prose that named the old behaviour fixed in the same pass (TypeFetcherGenerator:6303/:6498,MultiTablePolymorphicEmitter:2006). Coverage: execution-tierGraphQLQueryTest(filmsConnectionnegativefirst/lastsurface the argument-naming message and never containAn error occurred. Reference:,first: 2147483647surfaces the overflow message,first: 0returns an empty page with a computablehasNextPage, the migrated collision message, and the load-bearingfilmById(…) { actorsConnection(first: -1) }proving the async-arm flip end-to-end through the DataLoaderCompletionExceptionunwrap, which a root-connection test alone would pass without the flip), unit-tierErrorRouterClassGeneratorTest.noChannelRouterCall_emitsSurfaceClientErrorOrRedact, and updatedTypeFetcherGeneratorTestarm pins. Out of scope, filed nowhere yet: malformedafter/beforecursor redaction (same family, different surface) and a configurable maximum page size (a DoS policy cap, distinct from this correctness-only overflow guard). Builds on R378. Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db(GraphQLQueryTest294 tests, 0 failures) after re-seeding a stale localrewrite_testDB (missing the R389party+jti_*joined-table fixtures) and a clean jOOQ catalog regen, the sameQuery.allParties/JoinedTableInheritancePipelineTeststale-DB artifact the R416/R407/R182 Done reviews documented, on paths R415 does not touch. -
R416 (
71f5429In Progress → In Review implementation;d58ae37Dependabot #504/#506 closure record;d893673self-review charset-tolerant asset content-type assertions; Spec → Ready943884f, Backlog → Spec9ad3ff1, Backlogc3623f2): Self-host the GraphiQL playground assets ingraphitron-jakarta-rest, retiring the runtime unpkg CDN. The playground page (GET /graphql,Accept: text/html) previously loaded React + GraphiQL from unpkg at latest (unpinned, so it silently tracked upstream) and was dead behind a strict CSP or on an air-gapped network, unacceptable for "the first hand-written runtime artifact consumers depend on" serving Sikt’s gov/edu consumers. Now a version-pinned GraphiQL 5 + React bundle (graphiql@5.2.2,react@18.3.1,graphql@16.13.2,@graphiql/toolkit@0.11.3, built withvite@6.4.2/@vitejs/plugin-react@4.7.0) ships as plain classpath resources underno/sikt/graphitron/jakarta/rest/graphiql/and is streamed by a new path-traversal-safe@GET assets/{name}method onGraphqlResource(a[A-Za-z0-9.-]+allowlist + explicit..reject + extension→MIME gate overjs/css/map/ttf/woff/woff2/svg, all behind the existinggraphiqlEnabled()seam). Vendor-neutral by design:getResourceAsStream, notMETA-INF/resources/(which only serves on Quarkus, contradicting the module’s Jakarta-EE-neutral ethos).graphiql()gained@Context UriInfoand rewrites a{{ASSET_BASE}}placeholder to the absolute per-request…/graphql/assets/prefix, so the entry files resolve at any mount point (/graphql,/api/graphql, …) while every code-split chunk/worker/codicon-font resolves relative to them via the bundle’sbase: './'. The Vite recipe wasgit mv’d from the (now GraphiQL-free) `graphitron-sakila-exampleintographitron-jakarta-rest/tools/graphiql-build/as a one-shot commit-the-output recipe: no<build>binding, so the reactor’s "CI never touches node" property holds; the committed bundle is the artifact, the recipe the reproducibility receipt. Deviations from the plan, all documented and justified: a JS-entry Vite build (GraphiQL 5 is a monaco bundler SPA, not the old UMD global-script shape) rather than the literal "swap four URLs"; staying on vite 6 / plugin-react 4 rather than #506’s vite 8 / plugin-react 6 majors (plugin-react 6 drops Babel for Oxc, a larger change with no benefit here); the MIME map extended past the plan’s js/css/map to cover monaco’s codicon.ttfand fonts; the broader R399 app-section README drift (deadGraphqlEngine/GraphqlResource/AppContextlinks) split out as follow-up R417 rather than expanded into scope. The companion/opt-in-graphiqlartifact (the architect’s leanness-preserving "option C") was consciously deferred: the consumer set is small and known, so the bundle weight is accepted in the core jar now, with option C the escape hatch if a real consumer is pinched. Docs reconciled across every GraphiQL surface (thegraphiql.htmlrationale comment, the relocated recipe README,modules.adoc, both tutorial pages, the sakila-example README’s GraphiQL entries); Dependabot #504 (linkify-it) and #506 (vite/plugin-react) closed as superseded, each pointing at R416. Coverage per R399 (jakarta-rest carries no@Testclasses):graphitron-sakila-example’s `GraphqlResourceSmokeTestgained six page + asset conformance checks (self-hosted page has the mount div, nounpkg.com, and a resolved…/graphql/assets/base; the entrygraphiql.js/graphiql.cssstream with the right content-type; missing and unknown-extension names 404). Independent-session In Review → Done review; full reactor green undermvn clean install -Plocal-db(2416 core tests + the sakila-example compilation/execution tiers,GraphqlResourceSmokeTest6/6, 0 failures). Thegraphitron-core red the implementer reported as "pre-existing trunk breakage" (Query.allParties→UnclassifiedField,JoinedTableInheritancePipelineTest, and thePerson/AppAccountjoined-table schema-validation failures in sakila-example generation) was in fact the known stale-rewrite_test-DB artifact the R407 and R182 Done reviews already documented, the local DB predating the R389party+jti*fixtures so its jOOQ catalog lacked the detail tables' primary keys; re-seeding frominit.sql+ a clean catalog regen produced a fully green reactor and let the conformance test run in-pipeline, on paths R416 does not touch. -
R407 (
406f80dimplementation; In Progress → In Review2f5f741, Ready → In Progressd910a1f, Spec → Ready825b524, build-command fixupb483e1e): Exclude generator-injected federation/@linkdefinitions from the R398 SDL lint engine. A consumer schema carrying a federation@linksawtype-names-pascal-caseandtypes-and-fields-have-descriptionswarnings onfederationFieldSet,linkImport, and siblings the author never wrote and cannot rename (names dictated by the federation spec) or document (descriptions owned byfederation-graphql-java-support); these definitions carry anullsource, the tell that they came from no consumer.graphqls. Fixed by provenance, not aname.contains("_")heuristic and not by borrowingScalarTypeResolver.FEDERATION_NAMESPACE_SCALARS(a hand-maintained expectation free to drift on a spec bump):FederationLinkApplier.applynow returns theSet<String>of names it injected (collected in its existingdefs.forEachloop, the sole contributor) instead of a bare boolean.AttributedRegistrycarries that set asinjectedNames()and derivesfederationLink()from it ("injected anything"), collapsing the two facts into one component rather than a parallel carrier;AttributedRegistry.from(…)derives the set the same way for ad-hoc test registries.LintEngine.rungains an overload taking the set and unions it with the existingBUNDLED_TYPE_NAMESexclusion at the two skip points it already has, widening the name-set skip to a second generator-owned contributor with no new skip mechanism, no newLintRule, no newLintNodeKind.KeyNodeSynthesiseris untouched: it decorates author@nodetypes in place with@keyand injects no new definitions, so folding its names in would wrongly silence real author violations; it keeps itsvoidsignature. Coverage: pipeline-tierLintInjectedFederationDefinitionsTestcarries a federation@linkplus an authortype lowercase @node(the exact type synthesis decorates), exercising the real injection path and pinning both halves in one fixture, injected names stay silent while the author type’s pascal-case violation still fires; findings asserted on the typedLintRuleand the minimum node-identity check, no rendered-message or generated-body assertions;FederationLinkApplierTestupdated to assert on the returned name set. No per-name unit list. Reuses R398’sBUNDLED_TYPE_NAMESmechanism; sibling to R408 (author-driven suppression, the "chooses not to fix" half). Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db(2407 core tests + sakila-example compilation/execution tiers, 0 failures) after re-seeding a stale localrewrite_testDB (missing the R389party+jti*joined-table fixtures) and regenerating the jOOQ catalog, the sameQuery.allParties/JoinedTableInheritancePipelineTeststale-DB artifact earlier Done reviews noted, on paths R407 does not touch. -
R182 (In Review → Done; landing sequence
657dd71delete legacy reactor,a505cabunwrap rewrite to repo root,aad2f61restructure architecture docs into Diataxisdocs/architecture/,cbaeb64CI/CD workflows,31750bfdocs+tooling paths; In Review transitionb0cb778; In Progresse90e321; R19 discardadb071a): Retired the legacygraphitron-parentreactor and unnestedgraphitron-rewrite/to the repo root. The six legacy modules (graphitron-codegen-parent,graphitron-common,graphitron-example,graphitron-maven-plugin,graphitron-servlet-parent,graphitron-schema-transform) and the legacy rootpom.xml+maven-build.ymlare gone; the rewrite aggregator (graphitron-rewrite-parent) is now the root POM with its eleven modules +docsat the top level, and the duplicategraphitron-javapoetcollapses to one copy. This closes therelease-event publish hazard:maven-publish.ymlonmaindrops the-f graphitron-rewrite/pom.xmlflag and carries the RC-aware tag regex, so a release tag can no longer republish deleted legacy artifacts at 10.0.0. Docs restructured: the flatgraphitron-rewrite/docs/tree became a Diataxis-shapeddocs/architecture/{explanation,reference,how-to}/folded into the site module (README.adocsplit intoindex.adoc+reference/modules.adoc+explanation/pipeline-overview.adoc;getting-started.adocdissolved into the manual how-to pages plus a newhow-to/dev-loop-internals.adoccarrying the#dev-loop-detail/#native-runtime-dependencyanchors), and roadmap-internalworkflow.adocmoved out of the site toroadmap/workflow.adoc.roadmap-tool’s `Main.javagained a quadrant-awaremapAdocTarget(driven by anARCH_QUADRANTslug table) plus repointed README/status-board headers; the ~11 inbounddocs/**architecture xrefs were repointed; CI path prefixes and thetree-sitter-natives-release.ymlenv vars were de-prefixed.verify-standalone-build.shretired, the CLAUDE.md legacy-scope rule removed, andrewrite-design-principles.adoc’s standalone-vs-legacy invariant reworded now that no legacy tree exists. Closes R26’s last open sub-item and supersedes R19 (discarded, not squashed). Step 8 (cut a release tag to exercise the consolidated publish workflow end-to-end) is inherently post-merge: the hazard only closes once this lands on `main. Independent-session In Review → Done review; full reactor green undermvn install -Plocal-db(2406 tests ingraphitron, docs site renders the Diataxis/architecture/tree withworkflow.adoccorrectly absent). Thegraphitron-core red first seen in the review sandbox (Query.allParties→UnclassifiedField) was the known stale-rewrite_test-DB artifact (the pre-existing DB predated the R389partyfixture and jOOQ skipped codegen against it); re-seeding frominit.sql+ a clean catalog regen produced a fully green reactor, orthogonal to the R182 diff. -
R19 (discarded, superseded by R182): "Rebase and squash rewrite branch onto main" is abandoned. R182 retires the legacy reactor and unnests the rewrite by moving the tree up one level in three ordinary commits (delete, unwrap, docs restructure), not by rewriting history; R19’s squash approach is not the accepted path. R19’s own numbers were stale (April-2026 commit counts against a since-moved merge base). File deleted per the 2026-07-01 staleness audit.
-
R406 (landing
e72eb02; rework onto R405 trunk from first pass1b6971d, In Review → rework1f4412f, Spec → Ready6296d0b, spec5f5ebc2, filed1633934): Support a single-table discriminated interface (@table @discriminate, implementers pinned by@discriminator(value:), all sharing one jOOQ table, e.g.Contentovercontent) as a DML@mutation(typeName: INSERT|UPDATE)return type. Before this, such a return was not rejected but silently mis-accepted (aTableInterfaceTypeis aTableBackedType, so it classified through theTableBoundReturnTypearm toProjectedSingle/ProjectedList) and emitted a<Type>.$fields(…)re-projection that never generates for an interface, so the sources failed to compile. The write half is a plain single-@tablewrite (the discriminator is an ordinary@field(name: "CONTENT_TYPE")input column the client sets;resolveInputunchanged); the entire fix is on the return half. Model: added the siblingDmlReturnExpression.DiscriminatedSingle/DiscriminatedListarms carrying the read-side discrimination data (interfaceName,discriminatorColumn,knownDiscriminatorValues,TableBoundparticipants) sourced verbatim from theTableInterfaceTypeverdict, the DML sibling of R405’sServiceTableInterfaceField; both map to the sameRecord/Tabledomain-return + target shape asProjected. A new return-shape arm (not a per-verbMutationFieldleaf) keeps the fork off the write-verb axis, per "lift the fork into the model": the write half is uniform across INSERT/UPDATE and the model already carries the return-shape seam. Classify: the single DML chokepointbuildDmlFieldresolves the return’s look-ahead verdict once and threads it into the still-staticbuildDmlReturnExpression, which builds theDiscriminated*arm when the verdict is aTableInterfaceType. Validate:dispatchPerformsReFetchrecognises theDiscriminated*arms as re-fetching, keeping the emitter andOutputField.requiresReFetch()in lockstep under the build-time drift guard; the DELETE and@asConnectionfloors already fire for the interface case through the sharedTableBoundReturnTypearm. Emit: consumes R405’s shared read-side re-projectionTypeFetcherGenerator.buildTableInterfaceReprojection(passingList.of()foralwaysProject, since the DML path keys the follow-up SELECT by a PK-INConditionoff theRETURNINGkeys rather than re-mapping by PK like the service path); the duplicatebuildDiscriminatedReprojectionfrom the first pass is deleted. Step 1 (PK-onlyRETURNINGindsl.transactionResult) and the composite-safe PK-IN builder were extracted intoemitKeysTransaction+buildPkKeysCondition, R406-owned and shared only betweenemitProjectedandemitDiscriminated(the DML write half has no R405 equivalent; they key off theRETURNINGkeyslocal, distinct from R405’srecords-keyedMultiTablePolymorphicEmitter.buildPkInCondition). The generated row carriesdiscriminator; the interface’s existingTypeResolversetstypenameper row, so no new resolver and no per-typename UNION. Drop/write-read asymmetry aligned with R405: an INSERT of an unknown discriminator commits its row (the transaction closed before the follow-up SELECT) yet returnsnull, since the discriminator filter cannot name a subtype outside the known set; enforcing the discriminator domain is the database’s job (aCHECKconstraint), not a graphitron pre-screen. Scope: INSERT/UPDATE only; DELETE (encoded-ID, own floor), UPSERT (blocked on R144/R145), unions (permanently), and Connection stay out. Coverage: unit-tierGraphitronSchemaBuilderTest(DiscriminatedSingleINSERT + UPDATE +DiscriminatedListclassification, a regression pin against the pre-R406ProjectedSinglesilent-accept, and a DELETE-floor rejection pin), pipeline-tierFetcherPipelineTestINSERT/UPDATE shape assertions, and execution-tierDmlTableInterfaceReturnExecutionTest(real PostgreSQL over thecontentfixture + a newContentInput/createContent/updateContentschema fixture: per-typenamerouting off the live discriminator, the cross-tableFilmContent.ratingjoin, same-tableShortContent.descriptionisolation, and the unknown-discriminator write/read asymmetry). Independent-session In Review → Done review; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. The full-reactorgraphitron-core red first observed in the review sandbox (Query.allParties→UnclassifiedField;JoinedTableInheritancePipelineTestClassCast on the R389jti_*fixtures) was the known stale-rewrite_test-DB artifact (missingjti_app_account/jti_personcomposite PKs) compounded by jOOQ’s up-to-date codegen skip; re-seeding frominit.sql+ a clean catalog regen produced a fully green reactor including R406’s execution tier, and the failures are orthogonal to R406. Consumes R405’sbuildTableInterfaceReprojection(extracted read-side, landed7b9d051). Non-blocking carry noted at review: the twoFetcherPipelineTestcases useCodeBlock.toString().contains(…)body-string assertions (existing DML-fetcher precedent in the same file; model shape pinned inGraphitronSchemaBuilderTest, behaviour at the execution tier). -
R370 (core four sites + fixtures + R412 filing
a13523e; two remaining in-hand@service-path sites + witnesses4e22ee4; Spec → Ready7a853a5, In Review → Ready rework352cc16): A record-backed parent with a nested backing class emitted the non-compiling$-qualifiedOuter$NestedbecauseClassName.bestGuessre-parses a binary class name and never splits on$. Fixed at the six sites that already hold a structurally-correct name in hand, so the swap is a one-for-oneClassName.get(Class)/ captured-TypeNamesubstitution at the source boundary rather than a per-consumer patch: the twoAccessorRefproducers (deriveAccessorRecordParentSource,derivePolymorphicHubSource), the@servicereturn-type validator (checkServiceReturnMatchesPayload, previously spuriously rejecting nested payloads at classify time), and the query + mutation@servicefetcher return types (computeServiceRecordReturnType,computeMutationServiceRecordReturnType, both collapsed toServiceMethodCall.javaReturnType()so the twins no longer drift), plus the@serviceOutcome payload ctor arm (resolveErrorChannel→ClassName.get(payloadCls)). RestoresAccessorRef’s own javadoc contract and corrects the now-true "Identical policy" mirror javadoc; the stale R370 hazard note in `buildScalarPerParentFetcherwas removed. Coverage: two compilation-tier fixtures, one perAccessorRefproducer (NestedFilmsPayloadlist-arm viabuildAccessorKeyMany,NestedOccupantCarriersingle-cardinality polymorphic viabuildScalarPerParentFetcher); a mutation +@error-channel compilation fixture (NestedFilmReviewPayload) reachingcomputeMutationServiceRecordReturnTypeandresolveErrorChannel; and a classification-tierErrorChannelClassificationTest.childServiceRecordField_nestedPayloadBacking_payloadClassIsStructurallyResolvedpinning the resolvedPayloadClass.payloadClass()to the structuralOuter.Nestedby object-equality on theTypeName(no code-string assertion on any generated body). The review corrected the spec’s second-witness premise: after the R244 Outcome flip, root@serviceoutcome fields classify toErrorChannel.Mapped(no developer payload class emitted), soresolveErrorChannel’s `PayloadClassarm is reached only by a child@servicefield, hence the classification-tier witness rather than a sakila compile fixture. The remainingbestGuess-over-fqClassNameemit sites that hold no reflectedClass/capturedTypeName(backingClassOf,recordColumnReadArgs,FetcherEmitter, severalChildFieldsites) need a model-lift, not a call swap, and were filed as R412. Independent-session In Review → Done review; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(2406 core tests + the sakila-example compilation/execution tiers, 0 failures) after re-seeding a locally-clobberedrewrite_testcatalog (theQuery.allParties/JoinedTableInheritancePipelineTestcascade was a stale-DB artifact orthogonal to R370). -
R405 (landing
7b9d051; Spec → Readye133e55, Backlog → Specca05952, flow/mechanism capturef61566b, filed1633934): Support a single-table discriminated interface (@table @discriminate, implementers pinned by@discriminator(value:), all sharing one jOOQ table) as a root@servicepolymorphic return, closing the last deferred shape on the@servicepolymorphic surface. Route (a) (R365, multitable) dispatches each service-returned record on its runtime Java class, which cannot tell same-table subtypes apart; this path instead reuses the read-side discriminator mechanism. NewQueryServiceTableInterfaceField/MutationServiceTableInterfaceFieldleaves (single-table siblings ofServicePolymorphicField) carry the shared-@tableTableBoundReturnType, discriminator column + known values, andTableBoundparticipants plus the service binding; target shapeInterfacekeepsrequiresReFetch()false. Both root classifiers'TableBoundarms build the new variant when the verdict is aTableInterfaceType(deferServiceTableInterfacedeleted). Emit reuses the read-side projection: the shared discriminator-filter +discriminatorprojection + cross-tableLEFT JOINassembly was extracted from the two read fetchers into a package-privateTypeFetcherGenerator.buildTableInterfaceReprojection(read paths pass an emptyalwaysProject, output unchanged); the newMultiTablePolymorphicEmitterservice fetcher calls the service, normalises the return toList<Record>(extracted shared snippet), collects the shared table’s PKs into a composite-safeDSL.row(pk…).in(rows)condition, runs one by-PK SELECT through the shared helper, and re-maps rows to input positions by PK (drop contract aligned with route (a): an unmatched PK drops from a list, yieldsnullfor a single). Validate mirrors the single-table floor (validateCardinalityonly, not the multi-table participant check). Union returns stay permanently unsupported; child@servicepolymorphic returns and@asConnectionstay out of scope. Coverage: pipeline-tierServiceTableInterfaceReturnPipelineTest, unit-tier classification flip (serviceReturningTableInterface_classifiesAsServiceTableInterfaceField+ mutation twin) andServiceTableInterfaceFieldValidationTest, the twoClassifiedCorpusexamples +GeneratorCoverageTestleaf-partition pin, and execution-tierServiceTableInterfaceReturnExecutionTest(real PostgreSQL: list routes each row toFilmContent/ShortContentoff the live discriminator, populatesFilmContent.ratingvia the cross-table join, honours the drop contract, single + mutation cardinality). Independent-session In Review → Done review. R405’s own tests are green; the full-reactorgraphitron-core red observed in the review sandbox (Query.allParties→UnclassifiedField;JoinedTableInheritancePipelineTestClassCast) was a stale-rewrite_test-DB artifact (missingparty_tables +jti_personPK) compounded by jOOQschemaVersionProviderregeneration caching, reproduced identically at the pre-R405 parent8fca064and orthogonal to R405; re-seeding the local DB frominit.sql+ a clean rebuild produced a fully green reactor including R405’s execution tier. Non-blocking follow-up noted at review: the pipeline test usesCodeBlock.toString().contains(…)body-string assertions (repo precedent exists; behaviour is independently pinned at the execution tier). R406 reuses this item’s read-side dispatch for its DML return half. -
R332 (
cb99991implementation;8fca064fixture-gate accounting; Spec → Ready6ad75dd): Deprecation signal for@tableon input types, a signal-only precursor ahead of R97’s consumer-derived removal (classification behaviour unchanged). Two tiers per D1. Prose tier (ships unconditionally, carries the carve-out in words): the@tabledescription indirectives.graphqlsgains the input-type deprecation note + replacement instruction (no SDL@deprecatedmarker, the spec forbids it on a directive location); a directive-level row indeprecations.adocwith the section heading/intro widened from "whole directives" to "whole directives and directive locations"; a WARNING admonition on the canonicaltable.adocpage; and thecode-generation-triggers.adocInput type with @tablerow annotated. Actionable tier (fires per@table-on-input type, respects the carve-out): a post-classificationGraphitronSchemaBuilder.emitTableOnInputDeprecationWarnings(ctx)pass placed besiderejectCaseInsensitiveTypeCollisions, walkingctx.schemainput types that explicitly declare@tableand emitting a non-fatalBuildWarning.NoRuleper usage (the plain non-lint arm per D1, notLintFinding; message names noR<n>per D2; unconditional per D4). The encoded-ID / scalar-return INSERT/UPSERT carve-out (D3) is computed byencodedWriteTargetInputTypes(ctx)off the classified model (theMutationInsertTableField/MutationUpsertTableFieldleaves whosereturnExpression()is anEncoded*arm), needing nolookAheadVerdictor reflection, so R332 staysdepends-on: []; the type-level conservative rule suppresses an input reused by any encoded INSERT/UPSERT.encodedWriteTargetInputTypesis the named find-usages anchor R97 Phase 2b retires (forward edge R97 → R332). Coverage: pipeline-tierTableOnInputDeprecationWarningTest(projected-return INSERT warns with source location; encoded-ID INSERT carved out; D3 reuse suppression) 3/3 green;DeprecationsDocCoverageTestgains"table"inWHOLE_DIRECTIVE_DEPRECATIONS;FixtureWarningsGateTestsegregates the R332 category out of its exactly-one advisory scope (mirroring the ENGINE-lint filter) and adds a count-independent carve-out test (FilmCreateInput/FilmDeleteInputwarn, encoded-IDCreateKeyedNodeInputdoes not); no code-string assertions on generated bodies. Independent-session In Review → Done review;TableOnInputDeprecationWarningTestgreen and the emittedNoRulemessage shape matches both gate matchers by inspection. The twographitron-sakila-exampletests could not execute because that module’sgenerategoal is blocked on trunk by the pre-existing R389 fixture breakage (party/party_individual/jti_personcatalog tables absent,Query.allPartiesunclassified); the 11graphitron-core failures (JoinedTableInheritancePipelineTestClassCast + theQuery.allPartiescorpus cascade) were confirmed to reproduce identically with R332 reverted, so they route through R389, not this item. Remaining work: R97 Phase 2b empties the carve-out and lets the warning fire on encoded INSERT/UPSERT inputs, at which point this item folds or retires. -
R398 (
1d520faengine + nine visitors,82fd246sealedBuildWarning+ advisory tagging + MCP projection,148c8acLSP finding-keyed QuickFix,fd38fb0@recorddeprecation-marker alignment,53d475breport/LSP integration pins,d5b0a57In Progress → In Review,3a4a190explicit-fix decoupling; Spec → Ready3ee7062): SDL lint engine with ESLint-style built-in visitors. A single shared traversal over the build’s parsed graphql-java AST dispatches each node to theLintVisitor`s subscribed to its `LintNodeKind; adding a rule is registering it inLintRules, not editing a central switch. Rule identity is a type (LintRuleenum, stable kebab-caseid(), aSource{ENGINE, CLASSIFIER}axis), never a string bag. Findings ride the existing warning channel via a sealedBuildWarning(NoRulearm for the pre-existing untagged advisories;LintFindingarm carrying the typedLintRule+Optional<LintFix>), so a finding’s rule is a type and its fix lives only on the arm where it is meaningful, no nullable field; both arms flow intoValidationReportunchanged, so the LSP replay (Diagnostics.validatorDiagnostics, R139 freshness-silence intact) and the MCPdiagnosticstool project findings with no second evaluator, the MCP wire additionally carrying theLintRuleid. Nine syntactic engine visitors ship (type-names-pascal-case,field-names-camel-case,input-and-argument-names-camel-case,enum-values-screaming-snake-case,deprecations-have-a-reason,types-and-fields-have-descriptions,input-object-name-suffix,no-deprecated-directive-usage,no-typename-prefix); the three existing classifier advisories (splitquery-redundant-on-record-parent,redundant-record-directive,asconnection-same-table-pk-in) are surfaced and tagged at theirFieldBuilder/TypeBuilderemit sites, never re-derived, so the classifier stays their sole emitter and each coordinate is warned exactly once (no-deprecated-directive-usageexcludes@record, owned by the redundant-record advisory). OptionalLintFixis a suggestion the LSP turns into aQuickFixCodeAction(a new finding-keyed branch alongside the detector-drivenSdlActionspath, sharing only theWorkspaceEdit/TextEditemit primitives); the build never mutates SDL. Fixes are registered explicitly, never divined from a deprecation’s prose reason, and are offered only where the edit is provably safe within the document: additive inserts (deprecations-have-a-reason,types-and-fields-have-descriptions), local renames offered only for undescribed fields (field-names-camel-case,no-typename-prefix, since graphql-java reports a described node’s location at the description, not the name token), and bare-form-only safe deletions for the two ignored-directive advisories (@record(record: {…})has no computable end location, so it reports without a fix). Rename-class rules whose fix would ripple to references (type-names-pascal-case,input-object-name-suffix,enum-values-screaming-snake-case,input-and-argument-names-camel-case) ship no fix in v1. The pure graphql-javaDeprecationRecognizer(the@deprecateddocstring-token regex + native-marker read + typedDeprecationInfo{NATIVE, DOCSTRING}) is extracted down fromgraphitron-lspinto thegraphitronbuild module so visitor 8 can consume it build-side;LspVocabularynow delegates and keeps itsSchemaCoordinateadapter LSP-side (zero behaviour change, pinned byLspVocabularyTest/SdlActionDriftTest), and@recordgains the docstring@deprecatedmarker so the convention is uniform. Coverage: pipeline-tierLintEngineTest(per-rule positive/negative/range + fix edit-range pins),LintRuleRegistryCoverageTest(every ENGINE rule registered exactly once, no CLASSIFIER advisory in the registry, subscribed ∪ not-linted partitionsLintNodeKindwith no overlap/gap, mirroringVariantCoverageTest/EdgeCoverageTest),ClassifierAdvisoryFixPipelineTest(emit-site fixes + bare-only guard), LSP-tierLintQuickFixTest+ValidatorDiagnosticsTest(build-side finding replays into aWarningsquiggle at its range and applies to the corrected SDL, silenced on a stale snapshot per R139), and MCP-tierGraphitronMcpServerTest(both arms: no-rule advisory carries nolintRule, lint finding carries its id on the wire); findings asserted on the typedLintRule+SourceLocation, no code-string assertions on rendered diagnostic text or generated bodies. Deferred as designed: the plugin SPI, per-rule enable/disable + severity overrides + error-capable lint, a declarative rule-config DSL, reference-aware rename refactoring, and a second tree-sitter evaluator. Subsumes and retires R121 (redundant@splitQueryon@record) and R296 (deprecated-directive usage). Independent-session In Review → Done review; the full reactor is green under-Plocal-dbonce the localrewrite_testDB is seeded from the currentinit.sqland jOOQ regenerated (a stale sandbox catalog missing the R389partyjoined-table fixtures reproduces the sameQuery.allParties/JoinedTableInheritancePipelineTestclassification failures earlier Done reviews noted, on paths R398 does not touch). Approval corrected one false-invariant javadoc onLintNodeKind(it claimed a throw-on-unmapped instanceof chain the engine does not implement; the engine names the kind explicitly per dispatch site). -
R409 (
d2c8363implementation,9a22265In Progress → In Review; Spec → Ready972aeaf): Quiet the non-actionablemvn graphitron:devRAG-warm startup log noise and document the recommended consumer.mvn/jvm.configfor the warnings a plugin cannot un-print. A newRagLogQuietinghelper lives ingraphitron-mcp(where the RAG logger names are facts about the langchain4j-ONNX + Lucene dependency set R341/R372 dependency-quarantine, not knowledge the plugin’s compile surface should learn) and is called once fromDevMojo.bindServerbefore the warmsstart()on the dev thread, so thread-start’s happens-before edge publishes the suppression to thegraphitron-warm-daemon threads that load the noisy classes (in-code comment forbids reordering it afterstart()). Group 1 (DJL HuggingFace tokenizermaxLengthwarning): non-actionable, no public knob, so muted defensively across providers, the slf4j-simple per-logger level property *and the logger’s JUL level raised to SEVERE, so a Maven binding swap degrades to "noise returns" rather than a silent no-op; the DJL logger FQCNai.djl.huggingface.tokenizers.HuggingFaceTokenizerconfirmed against the1.16.3-beta26bge jar. Group 2 (LuceneVectorizationProviderincubator-module warning): actionable and directional, so demote-do-not-swallow, its JUL logger raised to SEVERE and, only whenjdk.incubator.vectoris absent (ModuleLayer.boot().findModule(…)), one concise graphitron-owned dev line names the--add-modules jdk.incubator.vectorflag in place of Lucene’s multi-line dump; present module → silent (fast path already on). Group 3 (Maven-runtime jansi native-access / guavaUnsafewarnings): documented only in getting-started’s new "Quieting startup warnings" note with a recommended.mvn/jvm.config, since the JVM prints them for Maven’s ownlib/jars before any plugin code runs. Helper javadoc names only what is attempted ("best-effort quieting"), never asserts the warning is gone, and pins the dev-goal-only scope (explicitly not shared withGenerateMojo/ValidateMojo, not triggered by theGraphitronMcpServerconstructor, not to be hoisted). Coverage: unit-tierRagLogQuietingTest(Lucene JUL SEVERE; DJL slf4j-simple propertyerror+ its JUL SEVERE; calling twice a no-op; the incubator-hint decision as a pure function of module presence) ; 5 tests, no code-string assertions on generated bodies. Dev-tooling plumbing: no sealed variant, no classification, no emitted Java. Independent-session In Review → Done review; R409’sgraphitron-mcp+graphitron-maven-pluginmodules compile clean andRagLogQuietingTestis green, and the full-reactorgraphitron-core failures (Query.allParties→UnclassifiedField;JoinedTableInheritancePipelineTestClassCast) were confirmed to reproduce identically at the pre-R409 parent commit5027e30, on classification paths R409 does not touch. -
R261 (landing
6d5ce8b, Spec → Ready7af72bf): Generation-time wire-coercion cast guard, Slice 1 (the three@servicearg-classification sites). Before this, every arg-classification site fell through toCallSiteExtraction.Directand emitted a raw(DeclaredType) wireValuecast that compiled cleanly andClassCastException`d (or, for enums, `IllegalArgumentException`d) on the first request, since graphql-java delivers `ID/enum asString,IntasInteger,FloatasDouble, input-objects asMap. The fix homes the "coercion output assignable to declared type" verdict at the classifier in a newWireCoercionResolver(a sealedPassThrough | Rejectedresult, mirroringEnumMappingResolver.EnumValidation), consuming a new pure forward mappingScalarTypeResolver.coercionOutputType(SDL scalar name → coercion-outputTypeName, over spec built-ins, federation scalars, and classified@scalarTyperesolutions), keeping the verdict offScalarTypeResolver(D1). A newWireCoercionErrorsub-seal ofRejection.AuthorErrorcarries two arms on two axes (D4):Assignability(coercion class ≠ declared type; sites A-D) andEnumConstantDivergence(declared type is the enum but an SDL value name has no matching Java constant; site E), each with a stablelspCode()undergraphitron.wire-coercion.wired intoDiagnostics.lspCodeOf,RejectionSeverityCoverageTest, andtyped-rejection.adoc(+ drift list).ServiceCatalog.argExtraction(site B) is widened to take the resolved SDL leaf and return a sealedResolved | Rejected;InputBeanResolver.bindField(sites A/E) calls the predicate on the scalar arm (widening R195’s jOOQ-record-only reject to the full wire-incompatible family) and routes the enum arm throughEnumMappingResolver.checkEnumConstants, an extracted column-agnostic single parity home reused byvalidateEnumFilter(column path) and the@serviceenum producers (D3). Coverage:WireCoercionCastGuardPipelineTestasserts per arm on the typed rejection’slspCode()/components (no code-string assertions on generated bodies), plus the ID→String, custom-@scalarType, and matching-enum non-regression cases that guard against over-rejection. Independent-session In Review → Done review; all R261-touched tests green and the wholegraphitron-lspmodule clean. Sites C (@condition) and D (@externalField) were carved into R411 (reject-wire-coercion-nonservice-sites,depends-ondimensional-model-pivot), which consumes this predicate unchanged; R261 droppeddimensional-model-pivotfromdepends-onand closes on the@serviceslice alone, with the@tableMethod/@conditioncaller deliberately left onServiceCatalog.legacyArgExtractionuntil R411 threads the predicate through R222’s channel. The sandbox’s pre-existingQuery.allParties/JoinedTableInheritancePipelineTestfailures (missing PK metadata on thejti_fixture catalog) were confirmed to fail identically at the pre-R261 parent commit6ad75dd, so they are not an R261 regression. (Landing commit message misnames the carve-out item as "R407"; the sibling item, spec body, and README all correctly reference R411.) -
R63 (landing
5ce8bc1, Spec → Ready9ea2a8c): Lift the DML UPSERT/UPDATE dialect requirement off hand-builtpostDslGuardCodeBlock`s onto typed model data. A new sealed `DialectRequirement(None/RequiresFamily/RejectsFamily) plus a graphitron-internalSqlDialectFamilyenum (a jOOQSQLDialect.family()collapse; name-prefixfromDialectNamecovers the commercial-onlyORACLE*/POSTGRESPLUSspellings the OSS jOOQ distribution omits) make the "UPSERT rejects Oracle" / "bulk UPDATE requires Postgres" facts discoverable onMutationField.DmlTableField.dialectRequirement()(never null), so the verb-neutralbuildDmlFetcherskeleton stays verb-neutral and a future validator can read the constraint at validate time. Each of the four DML records carries the component, populated at itsFieldBuilderconstruction site (UPSERT →RejectsFamily(ORACLE), bulk UPDATE →RequiresFamily(POSTGRES)selected oninputArg.list(), INSERT/DELETE/single-row UPDATE →None.INSTANCE);MappingsConstantNameDedupthreads it through the error-channel rebuild.buildDmlFetcher’s `postDslGuardCodeBlockparam becomes aDialectRequirement, collapsing three overloads into two, and a newemitDialectGuardhelper renders the guard. Divergence from the Ready draft (documented in the spec body and landing commit, and verified in review): the draft emitted a reference to the generator-internalSqlDialectFamilyenum into generated code, which does not compile in a consumer, wheregraphitronis test-scoped (graphitron-sakila-example/pom.xml) while the generated fetchers compile as the consumer’s main sources (GenerateMojoadds them viaproject.addCompileSourceRoot); the emitted guard instead stays self-contained, comparing jOOQ’s owndsl.dialect().family().name()against the family’sjooqFamilyName()(the reachable bulk-UPDATE output is byte-identical to the former inlinefamily().name().equals("POSTGRES")guard, and jOOQ’sfamily()folds everyORACLE*spelling toORACLE, so the UPSERT gate still catches them).fromDialectNameis retained on the model for the future validator-time check.postInGuard, the sibling free-formCodeBlockcarrying imperative per-row emission mechanics, stays aCodeBlock(documented non-goal: noDialectRequirement-shaped datum hides in it). Coverage: unit-tierSqlDialectFamilyTest(fromDialectNamecollapse +jooqFamilyNameincl. theOTHERrejection), pipeline-tierDmlDialectRequirementClassificationTest(per-verb population), emitterTypeFetcherGeneratorTest(self-contained Oracle guard from a directly-constructed field +Noneemits nothing; UPSERT can’t classify through the pipeline under R144), and the bulk-UPDATEFetcherPipelineTestassertion re-anchored to the typed guard; no code-string assertions on generated bodies beyond the intentional guard-shape pins. Independent-session In Review → Done review; all R63-touched tests green (213), and the sandbox’s pre-existingQuery.allParties/JoinedTableInheritancePipelineTestfailures (missing PK metadata on thejti_*fixture catalog) confirmed to fail identically at the pre-R63 parent commit. Carries forward the R22 post-shipping follow-up. -
R26: Umbrella tracker closed. Retiring
graphitron-maven-plugin+graphitron-schema-transformintographitron-rewriteshipped its build surface (schema loading, tagged inputs, Maven plugin, aggregator-standalone, content-idempotent writes) and@asConnectionemit-time synthesis; the Java LSP rewrite +devgoal landed under R18;@notGenerateddirective removal shipped on its own plan; Federation SDL integration continues under the separate Apollo Federation via federation-jvm transform backlog item; and the programmatic-schema architecture (Graphitron.buildSchema(…)) pruned type-extension merging, directive stripping, and client-SDL feature-flag splits from scope outright. Closed now rather than held open for its last sub-item, deletinggraphitron-maven-pluginwholesale and unnesting the rewrite aggregator, which continues as its own item, R182 (unnest-rewrite-aggregator.md); an umbrella tracker has no reason to outlive the work it was scoping once every other bullet under it has landed. -
R400 (Stage 1
0584430; Stage 2 page deletion27412d3+ xref strip2ea7900; In Review4771bcd; rework fix76d5ac9; spec/AC notescff3120/2eca156; In Review160ecef): Withhold the not-in-use directives from the v1 advertised directive surface, a docs-and-report-only trim with no generator behaviour change.DirectiveSupportReport.renderMigration(theroadmap-toolthat generatesdocs/manual/_generated/supported-directives.adoc) gained two curated policy sets:REJECTED_ON_USE= {notGenerated,multitableReference} moved out of "Supported" into a new "Removed / rejected directives" section that tells migrating consumers to delete them, andWITHHELD_FROM_V1= {tableMethod,sourceRow,experimental_constructType} silently excluded from "Supported" (declared and behaviourally unchanged, just outside the v1 surface).@recordstays advertised as-is (deprecated + silently ignored, kept for v1 per the 2026-06-30 user decision). Stage 2 deleted the withheld trio’s dedicated reference pages (reference/directives/{tableMethod,sourceRow,experimental_constructType}.adoc) and thehow-to/source-row.adocrecipe, and stripped every now-danglingxrefand teaching passage across the index and recipe pages while keeping factual inline-code mentions (the directives are withheld, not removed); the AsciiDoctor fail-on-WARN render is the guardrail that no danglingxrefsurvives.directives.graphqlsis untouched and no classify-time rejection was added. Recovery is anchor-free (git log --diff-filter=D+checkout <commit>^ — <path>) and ticketed under R403 (@tableMethodrethink + recover), R404 (@sourceRowrecover), R69 (@experimental_constructType, gated on an emitter). Independent-session In Review → Done review across two cycles: the first requested rework because deleting the trio’s pages while keeping them declared broke R68’sDirectiveDocCoverageTestdeclared-directive ↔ reference-page bijection (a hard build failure); fixed by76d5ac9, which narrows the invariant to "a directive needs a page only if it is on the advertised surface" and derives the exempt set from the generatedsupported-directives.adocfragment (declared − mentioned = withheld) so the test cannot drift from the report that ownsWITHHELD_FROM_V1. Coverage:DirectiveSupportReportTest(exclusion took effect: withheld trio + rejected pair absent from "Supported", rejected pair under "Removed / rejected",@recordretained) and the carved-outDirectiveDocCoverageTest; full reactor green under-Plocal-db. Absorbs and supersedes the upstream "Remove the @tableMethod directive" proposal. Spawns R403/R404. -
R389 (reshape
a3899f4; classifier/emitter/party fixture3586c06; pipeline tests6aa4bb3; compositejti_*re-authord08eacb; rejection tests521dd59; corpus + architecture-doc prosee163d59; In Review5451c0f; self-reviewbe47c31; schema-version rework3dc0a3a): First-class discriminated joined-table (class-table) inheritance, where each participant declares its own detail@tabledistinct from the discriminated base and its inherited (base-resident) fields carry@referenceback to the base. A newParticipantRef.JoinedTableBoundsealed variant carries the resolved child→parentJoinStep.FkJoin; residence is declared by the per-field@reference(base-resident →ColumnReferenceField, detail-resident → plainColumnField) and read off the field variant, never recomputed, so no residence-aware resolver was added toFieldBuilder.TypeBuilder.buildParticipantListdetects the detail-table participant, resolves the hop, skips the cross-table pass, and surfaces PK=FK / same-base / no-nameable-join violations asINVALID_SCHEMAdiagnostics with candidate-FK hints. The interface fetcher selectsFROM basewith a per-participant discriminator-gatedLEFT JOINto each detail table (base-resident + shared-key fields off the base, detail-exclusive fields off the detail alias, NULL-through for non-matching rows); the same concrete type stays first-class standalone, resolving inherited fields through the parent reference. Both shared-key shapes ship: a new single-columnparty/party_individual/party_companyfixture and the compositejti_*fixture re-authored to R389 (subsuming the R388 workaround). Coverage:@ExecutionTierallParties(routing + per-participant projection + NULL-through),allIndividuals(standalone), and the convertedallSubjectscomposite cases against real PostgreSQL;@PipelineTierJoinedTableInheritancePipelineTest(positive shape, mixed discriminator-only + joined participant, three rejection invariants); ajoined-table-interfaceR281 corpus example +code-generation-triggers.adocprose; no code-string assertions on generated method bodies. Independent-session In Review → Done review across two cycles: the first requested rework for a missingjooq.codegen.schema.versionbump (R389 changedinit.sqlbut left it at 2.2, so jOOQ skipped catalog regeneration on incremental-Plocal-dbbuilds → anUnclassifiedTypecascade across 11 tests); fixed by3dc0a3a(2.2 → 2.3) and verified with an incremental build that regenerates and is green. Builds on R388/R392; sets up R393 (base→detail join disambiguation). -
R399 (In Review
75c4894; JSON-B switche57bdbd; empty-body guardd6254dc): Newgraphitron-jakarta-restmodule, a reusable spec-conformant GraphQL-over-HTTP serving library over a Graphitron schema, ending the four-dialect drift between the reference app and thetilgangsstyring-style consumer copies (each had diverged on media types, status codes,/schema, and GraphiQL). The dependency-inversion seam is a consumer-implementedGraphitronApplicationSPI (schema(),newExecutionInput(), defaultengineBuilder(), defaultgraphiqlEnabled()) plus anAbstractGraphitronApplicationbase that caches the schema from a supplier lambda over the generated facade, so the library never names a per-subgraph type.GraphqlResource(@Path("/graphql")) owns POST/GET content negotiation, the media-type-driven status watershed (modernapplication/graphql-response+json: unparseable → 400, malformed/validation/coercion → 422, executed → 200; legacyapplication/jsonalways 200), GET mutation → 405,/graphql/schemaviaSchemaPrinter, and a CDN-based GraphiQL page; an application-scopedGraphqlEnginecaches the builtGraphQL. No custom JAX-RS providers: the resource reads the raw body and parses aGraphqlRequestrecord so it can shape parse errors as spec4xxand own the status watershed. Two refinements from the signed-off spec, both sound:graphiqlEnabled()rides the SPI interface (callable through the injected reference, overridable by a direct implementor), and body marshalling uses the Jakarta JSON Binding (JSON-B) API rather than Jackson, leaving the library with zero concrete JSON dependency (consumer supplies the provider; Yasson /quarkus-jsonb) consistent with its all-jakarta.-at-providedstance. The module is the first hand-written *runtime artifact consumers depend on, a third Java-version category (rewrite-design-principles.adocgrew the bullet): it compiles at<release>17</release>(the Java-17 floor consumers share), is publishable, and joins the deploy set.graphitron-sakila-examplerefactored onto it (itsGraphqlResource/GraphqlEnginecopies and self-hosted GraphiQL assets deleted, replaced by a one-classSakilaGraphitronApplicationadapter). Batching stays out of scope (the spec defines none). Coverage: an 11-case@ExecutionTierGraphQLOverHttpConformanceTestrun through the reference app exercises the real library end-to-end, one citing case per committed normative requirement, each carrying the verbatim spec sentence + section + revision as a@DisplayNameplus a requirement → section → test pointer table; the library itself carries no@Testclasses by design (keeps the per-module tier-enforcement in-scope list from growing). No code-string assertions on generated bodies. The most spec-load-bearing, least-typed spot,statusFor’s `422-by-exclusion arm, is documented with the unit-tier escape hatch should a graphql-java upgrade ever add a pre-executionErrorType. Independent-session In Review → Done review; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(clean build, 2313 core tests + all 11 conformance cases, 0 failures; an initial red run traced to a stale sandboxrewrite_testDB missing R389’sparty_*tables, resolved by a re-seed + clean rebuild, not a code defect in either item). -
R401 (Half A
8a08759+ dispatch fixf225eeb; Half Be6cbe70; In Reviewe288bf4): Bundle the tree-sitter runtime in the natives jar so the LSP has zero native system dependency.no.sikt:graphitron-tree-sitter-nativesnow shipslibtree-sitteralongside the grammar for all four supported platforms (eightlib/<os>-<arch>/entries), built in CI from the pinned upstreamv0.26.9source tag (POSIXmake; Windows MinGW-w64 with static-linked gcc runtime), gated on exported-symbol + transitive-dep allowlist assertions and apost-deploy-verifyload+parse on all four platforms with no systemlibtree-sitterpresent;0.26.9-1published to Central by a human-dispatched release.BundledLibraryLookupextracts both binaries and returnsgrammar.or(bundledRuntime)(system probe deleted);GraphqlLanguagecollapses the dual missing/too-old diagnostic apparatus (classifyInstalledRuntime,RuntimeStatus,runtimeProbePaths,tooOldRuntimeMessage,missingRuntimeMessage,ABI_VERSION_SYMBOL) to a single bundled-load-failure message naming the extracted temp path, withDOCS_URLrepointed to the reference page. Thelsp-requirements, getting-started#native-runtime-dependency, and reference-index docs collapse to "nothing to install" (per-platform install matrix and NixOSshell.nixsnippet removed). Coverage:NativeLibraryBundleTest(per-platform@EnabledOnOs; linux-x86_64 loads the bundled runtime + grammar via the SPI and parses),TreeSitterSmokeTest,GraphqlLanguageErrorTranslationTest(the single-diagnostic + classifier + missing-path contract). Independent-session In Review → Done review (approval commit also corrected the nativespom.xmlheader comment and the UPSTREAM.md Windows build command, which still described the pre-bundle grammar-only module); fullgraphitron-lspsuite + reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. Supersedes the system-dependency model from R203. -
R269 (
077f8f3implementation; Spec → Readyfa6f38d, spec revisions81ba814, spec refresh1ac70fb): Null-guard the record-parent split-query accessor key extraction so a nullable to-one/to-many@tablerelation that resolves to no row renders null/[]instead of NPEing.GeneratorUtils.buildAccessorKeySingle(theONEarm, the reported bug) read a nested jOOQ record off the parent backing via((Backing) sourceExpr).accessor()and called.into(<PK columns>)on it with no null guard, so when a nullable to-one accessor returnednullon an otherwise-successful parent the emitted fetcher threwCannot invoke "…Record.into(…)" because "element" is nullon the success arm rather than resolving the field;buildAccessorKeyManyhad the analogous hazard one level out, the bare for-each over a never-populated to-many backing NPEing before any.into(…). TheONEarm now emitsif (element == null) return CompletableFuture.completedFuture(null);between the accessor read and the key build, mirroring the FK-sidebuildKeyExtractionWithNullCheckprecedent (a key that can’t match the terminal PK must not dispatch the loader; the to-one’s faithful "no row" rendering isnull, and the fetcher’sCompletableFuture<DataFetcherResult<Record>>return makescompletedFuture(null)assignable). TheMANYarm took design fork (a): hoist the accessor result to a typedIterable<Element>local and skip the for-loop when it is null sokeysstays empty and the existingloadManydispatch renders[](the extraction block does not own its return path; bothDataLoaderFetcherEmitterandMultiTablePolymorphicEmitterappend their own dispatch, so skipping the loop is the consumer-agnostic shape). The asymmetry is deliberate and documented in both helpers: theONEarm preserves null-vs-present (a to-one’s "no row" faithfully rendersnull) while theMANYarm collapses null-vs-empty (a to-many has no surface distinction between "never populated" and "zero rows" once the loader returns). Element-level nulls inside a populated collection stay unguarded (a malformed backing, not a cardinality to model). Emit-only: no model/classifier/AccessorRef/SourceKeychange; producer/consumer linkage to the field’s nullability classification carried as a one-line rationale comment per the design-principles prescription (no validator invariant to mirror). The sharednull key → completedFuture(null)seam acrossbuildKeyExtractionWithNullCheck/buildAccessorKeySingleis named as a drift-prone follow-up, left out of scope as a refactor with its own blast radius. Coverage: execution-tierGraphQLQueryTest.inventoryById_filmCardDataNullAccessor_rendersFilmNullWithoutNpe(aFilmCardDataaccessor returningnullfor evenfilm_id`s renders `filmnull with no error, oddfilm_id`s still resolve their `Filmrow through the same loader, the mixed-batch proof) andAccessorDerivedBatchKeyTest.accessorDerivedManyPayloads_nullToManyBacking_rendersEmptyListWithoutNpe(aCreateFilmsPayloadwith a nullfilms()backing renders[], the present sibling still resolves); theRecordTableField+AccessorCall+Cardinality.ONE/MANYclassifier shape the guards depend on is already pinned byGraphitronSchemaBuilderTest’s `AccessorDerivedSourceCasematrix; no code-string assertions on generated bodies. Split out of R268 (the error-arm short-circuit) and orthogonal to it; R271 had already retired theelt/kdunders. Independent-session In Review → Done review; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(GraphQLQueryTest286,AccessorDerivedBatchKeyTest2, 0 failures). -
R262 (
c71885c, In Reviewc453aaf, Spec → Ready498ddf7, Backlog → Specd8ed14d): Reject@nodeIdon non-IDcoordinates and federation encoded@keyfields at validate time. The SDL directive permits@nodeIdonFIELD_DEFINITION | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITIONwith noIDrestriction, but every decode/encode arm is gated on"ID".equals(…), so a non-ID@nodeIdwas silently dropped and the raw base64 wire String bound undecoded: a green build with a production SQL bind/type error or never-matches predicate. Two build-time soundness reductions on the shared diagnostic channel (the R317-slice-5 / R204 / R194 pattern) close it.rejectNonIdNodeId(ctx)inGraphitronSchemaBuilder(sibling torejectCaseInsensitiveTypeCollisions, same orchestration region) walksctx.schemaapplied directives across all threeon-locations, object/interface field definitions plus their arguments plus input-object fields, and registers anINVALID_SCHEMAValidationErrorfor any whoseGraphQLTypeUtil.unwrapAllis notID(findings F, G, and the outputFIELD_DEFINITIONencode mechanism that has noIDgate at all); it reads the raw schema rather than the registry precisely because a dropped@nodeIdleaves no trace on the classified field.EntityResolutionBuilderrejects a federation@keywhose referenced field resolves to aChildField.ColumnReferenceFieldcarryingCallSiteCompaction.NodeIdEncodeKeys(finding H, where the_entitiesDIRECT path would bind the encoded global id undecoded into the VALUES table) fatally via a newAltResult.Fatalarm carrying the typedRejection.invalidSchema, distinct from and not co-located with the existing non-fatal compound-idBuildWarning; the column lookup is refactored fromlookupColumntocolumnOf(field)so the caller can inspect the field instance before reducing toColumnRef. Decode-into-rep for encoded@keyvalues, the cast-axis defects inwire-coercion-cast-guard, and consolidating the replicated@nodeId-site predicate stay out of scope. Coverage: pipeline-tierRejectNonIdNodeIdPipelineTest(6 tests, asserting the typedValidationErrorcoordinate +RejectionKind+ message for the non-IDinput field, argument, and output field plus the federation encoded-@keysub-case, and the legitimateIDinput/argument/output coordinates and federation NODE_ID happy path that must keep passing); no code-string assertions on generated bodies. Independent-session In Review → Done review;RejectNonIdNodeIdPipelineTestgreen 6/6 in isolation under-Plocal-db. NB: the full reactor is currently red on threeJoinedTableInheritancePipelineTestfailures owned by R389 (an unrelated In-Progress item that landed after R262,6aa4bb3); R262’s own delivery is independently green and those failures route through R389, not this item. Sibling of R397 (@erroron bare-entity query fields) and R273 (NodeId mismatch semantics). -
R378 (
3ca8428; Spec → Readye5dcd59, Backlog → Specf146e1e): Authored@nodeIdfilters now throw on a malformed or wrong-type id instead of silently dropping it to the unfiltered baseline (the reportedsoknadId: ["IKKE_EN_ID"]returns-the-whole-table bug). The four authored filter producers (argument-level same-table and FK-target inFieldBuilder, input-object-field same-table and FK-target inBuildContext) flip fromCallSiteExtraction.NodeIdDecodeKeys.SkipMismatchedElementtoThrowOnMismatch; the Relay heterogeneous-id-source pattern is given up deliberately per the user decision.CompositeDecodeHelperRegistry’s `Mode.THROWbody is enriched once (list and scalar arms) with a two-branch message computed fromNodeIdEncoder.peekTypeIdon the offending wire value:peeked == null || expectedTypeId.equals(peeked)reads as structurally-malformed ("not a valid<Type>id"), any other non-null prefix as well-formed-wrong-type ("decodes to type<got>, expected a<Type>id"); the expectedtypeIdis threaded ontoHelperRef.Decodeas a generation-time constant so the right-type-wrong-arity sub-case folds into the malformed branch. Error surfacing took path B (forward-compatible with a future query@errorlift, R397): a generated<outputPackage>.schema.GraphitronClientException(subclass ofgraphql.GraphqlErrorException, so natively aGraphQLError, channel-matchable, serialisable into theerrorsarray,serialVersionUIDemitted for clean-Xlint:serial) is the stable client-error marker the THROW arm raises; newErrorRouter.surfaceClientErrorOrRedactwalks the cause chain and surfaces aGraphitronClientException’s real message while still redacting genuine internal faults to a correlation id, and the no-channel catch disposition is repointed at it uniformly across `TypeFetcherGenerator(renamednoChannelCatchArm),MultiTablePolymorphicEmitter, andChannelCatchArmEmitter’s empty arm. Deliberate boundaries held: the two `_NODE*synthesis-shim arms stay onSkipMismatchedElement(R273/shim-retirement track), andLookupValuesJoinEmitter’s separate N×M decode-throw site keeps its plain `GraphqlErrorExceptionand still redacts (the R195/R315 record-decode boundary). No new validate-time rule (both arms and both registry modes were already fully implemented, so the flip introduces no unhandled classification; "validator mirrors classifier" satisfied vacuously). Coverage: execution-tierGraphQLQueryTest(malformed surfaces the real message naming the bad value +not a valid Film id; wrong-type surfacesdecodes to type "FilmActor"/expected a Film id; mixed surfaces; input-object-field filter surface exercises thesoknadIdshape; a genuine internal fault still redacts to a correlation id, pinning the surface arm narrows to the client-error type; empty-list R375 baselines retained), pipeline-tierNodeIdPipelineTest/IdReferenceShimClassificationTest(authored arms pinned toThrowOnMismatch, shim arms held atSkipMismatchedElement), unit-tierCompositeDecodeHelperRegistryTest+ErrorRouterClassGeneratorTest+GraphitronClientExceptionClassGeneratorTest(registry tests assert on code strings by that file’s existing convention; behaviour proven at the execution tier). Independent-session In Review → Done review; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(GraphQLQueryTest283,:graphitron477 /:graphitron-sakila-exampletiers all 0 failures). Orthogonal to R375; predecessor of R397. -
R121, R296 (Backlog items, discarded as superseded by R398): the redundant-
@splitQuery-on-@recordLSP diagnostic (R121) and the deprecated-directive-usage BuildWarning (R296) are folded into R398’s starter lint-visitor set (visitors 9 and 8 respectively). Neither shipped a standalone implementation; their intent moves wholesale into the R398 SDL lint engine, which evaluates such rules build-side and projects them into the LSP. The build-tier@splitQuerywarning R121 layered on already exists (FieldBuilder.warnIfSplitQueryOnRecordParent); R398 formalizes it as a visitor and adds the edit-time surface. IDs R121 and R296 are retired and not reused. -
R395 (
27325cd3eimplementation, fixture-comment rework7febb70b3; Spec → Ready05e652893, Backlog → Specbd0287f3e): Qualify the discriminated-interface discriminator column off the FROM table’s own jOOQ instance, not the@table(name:)directive string. R388 had changed the three discriminator SQL-emission sites inTypeFetcherGenerator(thediscriminatorrouting projection inbuildInterfaceFieldsList, the… IN (knownValues)restriction inbuildDiscriminatorFilter, and the cross-table LEFT JOIN ON-clause gate inbuildCrossTableJoinChain) to qualify viatableRef.tableName(), the verbatim case-preserved directive string. jOOQ renders the FROM table by its real schema-qualified, case-folded catalog name, so whenever the directive name differed in case or schema the qualifier did not match FROM and Postgres rejected the query withmissing FROM-clause entry(reported against10.0.0-RC21by the opptak consumer:@table(name: "INNBOKS_MELDING")overkommunikasjon.innboks_melding). All three sites now emit<tableLocal>.getQualifiedName().append(DSL.name(col))typedObject.class(so the.as/.in/.eqchains compile), producing the exact qualifier jOOQ renders in FROM by construction;tableRef.tableName()no longer reaches any discriminator site, and the read-side fourth site stayed correct (R392 had routed theTypeResolveroff the syntheticdiscriminatoralias). The default-schema path does not over-qualify because the rewrite sakila tables sit in jOOQ’s unnamed default schema (getQualifiedName()contributes no schema part). Coverage: four regression-lock unit assertions inTypeFetcherGeneratorTestpin each site to the table-instance qualifier and forbid the directive-name string via a case-mismatchedINTERFACE_BASEfixture (the case/schema-mismatch dimension lives here); a non-default-schema execution guardMultiSchemaQueryTest.signalsRouteToDiscriminatedTypesUnderNamedSchemaover a newmultischema_a.signal@discriminateinterface (AlertSignalcarrying a cross-table@referencetowidget,NoticeSignal) covers the dimension R388 regressed on; the default-schema guardsPolymorphicProjectionQueryTestandGraphQLQueryTestallContent/allSubjectsconfirm no over-qualification. Documented deviation: the execution fixture uses the unqualified@table(name: "signal")rather than the spec’s schema-qualified / upper-case form, which a separate@referenceFK-connection check (filed as R396) rejects; the unqualified directive still renders FROM as"multischema_a"."signal"and so still fires the pre-fix bug, matching the reported consumer shape. In Review → Done reviewed by a session distinct from the implementer; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R386 (
c68b000implementation; Spec → Ready270b0c1, Backlog → Spec3c6430b, filed27d9c9e):catalog.searchMCP tool (R118 slice 10) ; the semantic counterpart to the structuredcatalog.tables/catalog.describe, giving an MCP-aware agent fuzzy natural-language discovery over the database catalog ("where are customer addresses stored?") so a developer pointing graphitron at a large existing schema finds tables without knowing their SQL names.CatalogDescriptorsis a pure, ONNX-free composer that turns each R362CatalogFacts.Tableinto one readable descriptor carrying both the raw SQL token (so BM25 matchesfilm_actorexactly) and its normalized words (a state-machinesplitWordssplitting snake_case / camelCase / acronym / digit runs ;customerID→ "customer id",IDColumn→ "id column",address2→ "address 2" ; the model-agnostic retrieval lift of R118 OQ3), degrading to names-only when jOOQ captured no comments (OQ4); the SHA-256corpusHashis length-prefixed over the exact descriptor strings handed toembedDocuments, so the hashed thing and the embedded thing cannot drift.CatalogSearchIndexowns the warm-managed, self-observing Lucene index, mirroring R374’sReverseEdgeIndex.Cache(noBuildArtifactscomponent, noWorkspacefield, noDevMojolistener): eachsearchreads the livecatalogFactsthrough two gates ; reference identity (the cheap common path), then content hash (a no-op recompile that swaps the reference but not the content re-embeds nothing) ; and a changed hash kicks anAsyncWarmre-embed off the classpath-watcher thread, re-entering the existingWarmState.Warmingshape (no new "refreshing" state) so the priorReadyindex keeps serving while the new one builds. The index persists under${project.build.directory}/graphitron-mcp-rag/catalog/<corpusHash>/as a LuceneFSDirectory(survivesdevrestarts, dies onmvn clean), with an embedder-identity manifest (getClass().getName()+dimension()) written beside each index so loading a futuremultilingual-e5-smallindex under the English bge embedder (both 384-dim, indistinguishable by dimension alone) is rejected and rebuilt rather than silently mis-served (closes the R118 OQ2 cross-model trap now); sibling hash dirs are reaped keeping the current plus one prior. The tool takesquery(required) +limit(default 10), returns{status, results:[{id, schema, name, comment?, score}]}by the same schema-qualified SQL idcatalog.describeaccepts (discovery hands off to description), and returns the sharedWarmState.degradationMessage+{status: warming|failed}while the index is notReady. Threaded throughGraphitronMcpServer’s widened five-arg constructor (`RagConfigas a growable record, back-compat overloads default it to a temp dir) andDevMojo.bindServer(supplying the build-dir cache root); the multilingual swap is flagged, not done. Coverage: unit-tierCatalogDescriptorsTest(split normalization cases, comment-present vs name-only degradation, raw+normalized tokens, length-prefixed hash stability/segmentation) andCatalogSearchIndexTestoverFakeEmbedder+FSDirectory(hash-gated re-embed via an embed-call-count spy, warming-on-change re-entry, embedder-identity rejection-then-acceptance, persistence round-trip + current-plus-one-prior reaping, cross-warmFailedpropagation), MCP-handler-tierGraphitronMcpServerTest(catalog.searchadvertised intools/list; a ready-arm call returns rankedschema.tableids whose top feeds a follow-oncatalog.describe; the warming-arm call returnsstatus: warming; structured-content assertions only), and infrastructure-tier@Tag("slow")CatalogSearchOnnxTest(real bge ONNX embeds a Sakila-shapedCatalogFacts, assertspublic.address/public.paymentrank for natural-language queries ; the retrieval-quality + normalization payoff pin); no code-string assertions on generated bodies.getting-started.adocgains the agent-facing tool note (semantic search, the warming/refresh behaviour, the jOOQ-comment-capture lift). Builds on R372 (RAG foundation:Embedder+ the LuceneEmbeddingStore), R362 (CatalogFacts+ the schema-qualified ids), and R341/R361 (thegraphitron-mcpmodule + the shared-model / dev-trigger seam); sibling of R385 (docs.search, slice 9). In Review → Done reviewed by a session distinct from the implementer; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R392 (
d832aaa, In Review flip2a808d4): Route the discriminated single-table interface (@table+@discriminate)TypeResolveroff a synthetic discriminator alias instead of the raw column name. R388 qualified the three SQL emission sites but left a fourth, read-side site: the generatedTypeResolverread the discriminator with a barerecord.get(DSL.field(DSL.name(col))). When the interface also exposes the discriminator as a queryable field, the participant$fieldsprojects the real catalog column too, so the result carries the discriminator twice (the two-part routing add and the three-part schema-qualified column); the bare read matches both and jOOQ logsAmbiguous match found, resolving to the first by luck. Fix mirrors the multi-tabletypenameconvention: a sharedMultiTablePolymorphicEmitter.DISCRIMINATOR_COLUMN = "discriminator"constant (declared with its collision rationale, reaching generated code only as string literals ; a.as("discriminator")projection and arecord.get(DSL.name(…))read), projected under that alias inTypeFetcherGenerator.buildInterfaceFieldsListand read back inGraphitronSchemaClassGenerator, so routing is unambiguous and the user-facing discriminator field still resolves from its own column. The WHERE filter and LEFT JOIN ON-clause keep referencing the real qualified column (unaffected). Coverage: execution-tierGraphQLQueryTest.allSubjects_discriminatorFieldInsideFragment_routesViaSyntheticAlias(discriminator field selected inside the inline fragment; asserts routing per type plus theas "discriminator__"projection viaSQL_LOG) and the schema-generator unit testbuild_typeResolver_routesOffSyntheticDiscriminatorAlias(alias read replaces the raw-column read, per that class’s documented runtime-dispatch-infrastructure body-content exception). Residual hole left by R388; first-class per-participant@tablejoined-table inheritance remains R389. Independent-session In Review → Done review; full reactor green under-Plocal-db. -
R383 (
6df5616implementation, In Review flip0770918; Spec → Ready0657369): Support nested-input@fieldfilters on multitable interface/union root queries. R363 lowered@field-mapped filter inputs onto multitable interface/union fields but scoped day one to the branch-safe top-level extractions (Direct/EnumValueOf/ContextArg), so the idiomatic input-object shape (occupants(filter: OccupantFilter)withOccupantFilter { firstNames: [String!] @field(…) }) lowered to aNestedInputField(filter → firstNames, leaf)call-site extraction thatFieldBuilder.firstUnsupportedFilterArgrejected at classify time with an author-error, even though the leaf is a plain scalar; consumers hit it as a hard build failure on an ordinary filter-input schema. The fix is a classifier relaxation only, no registry / lift-context plumbing:FieldBuilder.isBranchSafeExtractionbecomes a recursive switch where aNestedInputFieldis branch-safe exactly when itsleafis (Direct/EnumValueOf/ContextArgadmitted; aNodeIdDecodeKeysleaf stays rejected through the recursion, a developer@conditionstays rejected by theGeneratedConditionFilterguard), and the switch is exhaustive over the sealedCallSiteExtractionwith nodefault(the five non-branch-safe permits listed as explicitfalsearms) so R384 lifting one fails to compile and forces a deliberate decision at this gate. The "this list-typed nested leaf extracts as(List<X>) map.get(key), an unchecked cast" fact is lifted onto the model asCallParam.emitsUncheckedCast()(single source of truth, Generation-thinking); both hosts (MultiTablePolymorphicEmitter’s `buildMainFetcher/buildRootConnectionFetcherand the single-tableQueryConditionsGenerator) fold over their call params and ask the model rather than each re-derivinglist() && instanceof NestedInputField, so R384 adds its unchecked-emitting arm in one place and neither host can drift. The condition-method generator is extraction-agnostic, so the generated<Participant>Conditionsmethod is byte-identical whether the value arrives top-level or Map-traversed. Coverage: pipeline-tierMultiTableFilterLoweringTest(nestedInputFieldFilter_lowersPerParticipantWithNestedExtractionasserts theNestedInputField(filter → firstNames, Direct)call param per participant;nestedInputFieldCondition_rejectedStructuralNotDeferredkeeps a nested-input developer@conditiona structural author error) and execution-tierMultiTableFilterExecutionTestoverAddressOccupant = Customer | Staff(occupantsByFilter(filter: { firstNames: […] })filters per branch and returns one matching row each, an empty filter narrows by nothing), with a new sakilaOccupantFilterinput +Query.occupantsByFilterfixture; no code-string assertions on generated bodies. The converted (JooqConvert/ ID-typed),@nodeId-decoded, and developer-@conditionkinds remain deferred to R384, which carries the registry / FK-target-alias plumbing they need. Builds on R363; sibling of R384. In Review → Done reviewed by a session distinct from the implementer; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R390 (
5485578, In Review flip92e7e16): Retain the connection carrier element subgraph in the rebuilt assembled schema. A@tabletype reachable only through a directive-driven@asConnectioncarrier was pruned bySchemaTransformeronceConnectionPromoter.rebuildAssembledForConnectionsretyped the bare-list carrier to name its synthesised Connection: the element type then hangs off the Connection’snodes/ Edge’snodeGraphQLTypeReference`s, which the transform treats as leaves, so an element reachable nowhere else (a nested-only chain), and its whole transitive subgraph, dropped out, either silently (its `<Type>Typeschema class never emitted, the consumer’s RC20cannot find symboljavac failure) or as an NPE in the type-reference resolver when a surviving typeRef still pointed at it; because the element was never traversed, a nested carrier’s own@asConnectionrewrite was also skipped, leaving that field a bare list while its fetcher was connection-shaped (the report’s variant-3 mismatch). Fix pins each rewritten carrier’s element type (resolved off the pre-rewrite schema bycarrierElementType, null-degrading when the parent/field/base type can’t be found) as aGraphQLSchema.additionalTypebefore the transform, deduped by name alongside the synthesised types; every carrier at every nesting depth is in the rewrite set, so pinning the direct carrier elements keeps each element and its concretely-reachable subgraph alive and every nested rewrite applies. Structural (SDL-declared) connections are untouched (no typeRef introduced, element stays concretely referenced). Coverage: pipeline-tierNestedConnectionElementRetentionPipelineTest(2: aStore --@asConnection-→ Customer --@reference-→ Paymentnested-reference chain retains both element types in the rebuilt assembled schema; a--@asConnection-→nested chain retypesCustomer.paymentstoCustomerPaymentsConnection!rather than leaving a bare list, with the synthesisedConnectionTypeclassified), structural assertions only, no code-string assertions on generated bodies. In Review → Done reviewed by a session distinct from the implementer; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R385 (
b0afcf8implementation,6cb322aREADME regen,d751110rework, In Review flipee71c81; Spec → Ready5cdcbe5, Spec2f014ce, filed27d9c9e):docs.searchMCP tool (R118 slice 9) ; build-time.adocchunking + a pre-embedded bundled index, async-loaded at startup for semantic retrieval over the public manual, and the first slice to wire R372’s async-warm lifecycle into the running server +DevMojo.AdocChunkeris a pure(adoc, sourcePath) → List<DocChunk>that walks raw.adocheading syntax (no AsciiDoctor render, so it stays off the docs module’s JRuby cost), splitting on section boundaries with an opt-in// rag:splitoverride, fenced-block awareness (==inside a----listing is body, not a heading), explicitoverride, and a heading-path breadcrumb prepended to each chunk’s embed text so a passage keeps its context.DocsIndexBuilder(bound toprocess-classesviaexec-maven-plugin, reading the public manual under a declared<docs.source.dir>) chunks + embeds viaBgeEmbedder.embedDocumentsand writes the bundle totarget/classes/mcp/docs-index/, gated by a SHA-256 content-hash stamp so an unchanged inner loop skips the ONNX cost. Bundle divergence (settled at Spec): rather than a literal LuceneFSDirectory(unreadable from a jar, would force temp-dir extraction),DocsBundlepackages pre-embedded(id, embedText, payload, vector)tuples behind a(magic, version, dimension, count)header with explicit byte-length string prefixes (past thewriteUTF64 KB ceiling); the docs warm rebuilds an in-memoryLuceneEmbeddingStore.inMemory(dimension)by re-add()-ing the tuples, re-embedding nothing at runtime. The store-opaquepayloadis a dependency-free URL-safe Base64 encoding (not the spec’s literal "payloadJson") ; a JSON parser would widen the module’s quarantined RAG dependency surface for a string only this module produces and consumes; this is the slice’s one principled divergence and is documented inline.DocsSearchToolembeds the query (embedder.embedQuery), runs theEmbeddingStoreseam, and returns ranked passages withheadingPath/sourcePath/anchor/text/score/ ahttps://graphitron.sikt.no/…;deep link; a once-memoised dimension guard reconciles the runtime embedder against the bundle’s build-time width and degrades cleanly on skew rather than throwing an opaque Lucene KNN error; either warm absent or not-Readyreturns the sharedWarmState.degradationMessageand no hits, leaving the dev loop structured-only. Wired throughGraphitronMcpServer’s new structured-only / injected-warm two-constructor seam and owned by `DevMojo. Rework (returned to Ready atdf7525a, re-landed atd751110): the first In Review pass failed the gate on a red build ;DevMojoTestpaid a realBgeEmbedderONNX load that SIGSEGV’d the surefire fork, and the MCP-bind-failure catch arm leaked the warms it had started. Fixed with a package-private warm-factory seam onDevMojo(mirroring the server’s injected-warm pattern;mojoFordefaults to structured-only null warms, the bind-failure test swaps in ONNX-free fakes) and anawaitAndCloseWarms()that joins each warm to its terminal state and closes the warmed docs store on the bind-failure unwind before rethrowing. Coverage: unit-tierAdocChunkerTest(5: nested heading paths,// rag:splitoverride, malformed-rag:-comment near-miss is body not a split, fenced-block heading-syntax is body, explicit anchor override),DocsBundleTest(3: write→read count/ids/dimension/vector-width round-trip, separator-collision-safe payload decode, header-onlyreadDimension), pipeline-tierDocsRagWarmPathTest(the productionloadDocsIndexloader end-to-end), MCP-handler-tierGraphitronMcpServerTest(warming/failed/ready/dimension-mismatch cases, structured-content assertions), andDevMojoTest(the bind-failure unwind leaves no live warm and freed the docs store); the build-time ONNX embed itself stays out of the fast suite (R372’s@Tag("slow")BgeEmbedderOnnxTestcovers the real load).getting-started.adocgains a paragraph. Builds on R372 (RAG foundation) and R341/R361 (thegraphitron-mcpmodule + live-Workspaceseam); sibling of R386 (catalog.search, slice 10). In Review → Done reviewed by a session distinct from both implementer sessions; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(DevMojoTest 6, GraphitronMcpServerTest 37, AdocChunkerTest 5, DocsBundleTest 3, DocsRagWarmPathTest 1; no SIGSEGV). -
R379 (
6f68611; Spec → Ready713d464, spec revise07c7dd3, Spec/filingdb865ef/1d8a78f): Validate that an@referencepath’s joins compile, moving twojavac-in-generated-code failure modes to build-time classification. Check 1 (terminal hop lands on the return table):InlineTableFieldEmitter.buildArmfeeds the terminal hop’s alias to a$fieldsoverload typed for the field return type’s@table, so a terminal hop landing elsewhere (theNusGrupperingFagfelt @table("NUSFAGFELT")reached via a path ending onNUSFAGGRUPPEreproduction, found in a downstream subgraph build) compiled to an incompatible-types error in a consumer’s build.BuildContext.parsePathnow computes a typedTerminalTargetVerdict(Match/Mismatch(fieldName, terminalTableName, returnTableName)/NotApplicable) over R232’s already-resolved terminalJoinStep.HasTargetTable.targetTable()and threads it ontoParsedPath, never re-deriving the hop kind from the directive element; theMismatchdiagnostic is formatted from the record’s fields so message and projection cannot drift. Check 2 (condition-method parameter tables): a two-argument condition method (aConditionJoinON clause or anFkJoin.whereFilter) emitted positionally asmethod(sourceAlias, targetAlias)that concretely types a jOOQTableparameter must match the alias the emitter passes it;validateConditionParamTables/checkConcreteParamTablecheck parameter 0 against the hop source and parameter 1 against the hop target at each condition resolution site, skipping the idiomatic wildcardTable<?>signature (unverifiable, fully accepted) and routing mismatches through the existingerrors→Rejection.AuthorError.Structuralchannel. Deviation (scope correction, agreed at review): the spec’s draft said to self-reject insideparsePath, butparsePathis shared by callers (@tableMethod,@nodeId,RecordTableField) that carry their own terminal-target checks (FieldBuilder.java:4692/:5778); self-rejecting there would preempt them and re-introduce the Generation-thinking same-predicate-two-consumers smell the item cites. Check 1 instead threads the typed verdict and rejects only at the two inline output callers (TableBoundReturnType,TableInterfaceTypeinFieldBuilder), the sole emit shape carrying the$fields(terminalAlias)invariant, double-gated on non-null start (excludes@sourceRow) and non-null return table (excludes input-field sites). The verdict is also the typed hook R381 Slice B consumes. Coverage: pipeline-tierReferencePathTerminalTargetTest(7: terminal{table:}/{key:}/ multi-hop landing on the wrong table rejected with pointed diagnostics, happy-path mirrors, a pre-existing mid-path-disconnect regression fence) andReferencePathConditionParamTest(6: both carriers and both parameter positions rejected, wildcard(Table<?>, Table<?>)and matching-concrete happy paths) over deliberately-mistypedTestConditionStubfixtures, message-content assertions only, no code-string assertions on generated bodies; no emitter behaviour change. In Review → Done reviewed by a session distinct from the implementer; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R363 (
2ede6e4implementation,73bc057self-review scope-tightening; Spec → Ready66cb717, spec revises6a3076d/50a4db9/413753a): Lower@field-mapped filter inputs onto root multitable interface/union query fields, closing a data-correctness leak where a filtered slice was requested but the rewrite emitted a bareUNION ALLand returned every row. The filter surface is per participant, not a single shared list: the same logical@fieldfilter resolves to a different table-specificWhereFilterper participant (e.g.FeideApplikasjonConditions.…vsMaskinportenApplikasjonConditions.…), so a new field-localmodel/ParticipantFiltersrecord pairs eachParticipantRef.TableBoundwith the filters lowered against its own table; the carrier is not a component on the type-scoped sharedParticipantRef, and the two fields stay offSqlGeneratingField(their return type isPolymorphicReturnType, a sibling of theTableBoundReturnTypethe capability’sreturnType()requires, soimplementswould not compile).FieldBuilderlowers once per table-bound participant viaresolveTableFieldComponentsagainst the participant table with a participant-named conditions class (participant.typeName(), notelementTypeName, so the per-participant methods do not collide), surfaces any participant’sRejected(absent or type-incompatible column) as the field’s rejection, and dedupes the@asConnectionsame-table advisory across the N participant calls.@condition(field- or argument-level) is rejected before the per-participant loop with a non-deferredRejection.structural(no danglingplanSlug), sinceresolveTableFieldComponentsitself lowers a@conditionbound to whatever table it is handed; guarding after the loop would pin the developer’s single-table method to the wrong table on N-1 branches.MultiTablePolymorphicEmitterthreads a typename-keyed filter map into both branch loops, ANDing each participant’s predicate into itsstage1_<Type>branchWHERE(buildStage1Blockcombines it with the existing parent-FK predicate;buildStage1ConnectionBlockgains a per-branchWHEREit never emitted before);TypeConditionsGeneratoris wired to the polymorphic fields'participantFilterssince they are notSqlGeneratingField. The self-review pass narrowed day-one extraction scope to the branch-safeDirect/EnumValueOf/ContextArgkinds:FieldBuilder.firstUnsupportedFilterArgstructurally rejectsJooqConvert(deprecated-for-removalDataType.converttrips the consumer-Werror),NodeIdDecodeKeys(needs theCompositeDecodeHelperRegistry), and nested-input / developer@conditionfilters (the classifier-guarantees-emitter-assumptions floor that letsbranchFilterWheredrop all registry/alias plumbing), with R383 filed for lifting the rest.orderByis split to R382 (sortdoubles as the connection cursor seek key). Coverage: pipeline-tierMultiTableFilterLoweringTest(per-participant lowering for interface + union; absent-column rejection; ID-typedJooqConvertrejected structural; field- and arg-level@conditionrejected structural, not deferred) and execution-tierMultiTableFilterExecutionTest(AddressOccupant = Customer | Staff, list +@asConnectionforms filter per branch and return only matching rows, exercising bothbuildStage1BlockandbuildStage1ConnectionBlock); no code-string assertions on generated bodies. SharesMultiTablePolymorphicEmitterwith R365/R366/R367. In Review → Done reviewed by a session distinct from the implementer; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R374 (
5193fdc; Spec → Ready44b0146, Backlog → Specfbb6c1f, filedb106ccc): MCP cross-reference edges (R118 slice 7): the traversal layer over the frozen R362/R368 structured tools, all module-local tographitron-mcp. A newedgestool (D-A, a dedicated tool rather than aneighboursfield retrofitted onto every result, so the eight existing contracts stay frozen) takes exactly one node selector (field/type/table/column+table/method/class) plus adirection(out/in/both) and returns that node’s typed neighbours. D-D: a sealedNodeRefmodel (TypeNode/FieldNode/TableNode/ColumnNode/MethodNode/ClassNode) owns the whole stable-ID grammar and composes each wire string only at theMcpWireboundary (methodRef/ newcolumnId), reconciling the classifier’s bare table names throughCatalogFacts.resolve(the qualifiedschema.tableIDscatalog.describeaccepts) and its arity-free(class, name)method pairs through the external-reference scan (oneRESOLVESedge per overload). D-B: anEdgeKindlabel enum (BACKS/TARGETS/REFERENCES/RESOLVES/PARTICIPATES) with the varying endpoint shape pushed entirely intoNodeRef.target, so the enum carries no kind-dependent nullability (the sealed-over-enum tension resolved); theTARGETS/REFERENCESsplit falls out of the classifier’sjoinPathdistinction rather than being re-derived. The arm-to-kind mapping is an exhaustive no-defaultswitchover everyFieldClassification(28) andTypeClassification(23) permit, mirroringSchemaView; the cross-module drift guard: a new classifier permit fails the edge switch to compile. D-C: a lazy,(snapshot, catalogFacts)-reference-pair-memoisedReverseEdgeIndex(the slice’s real deliverable, for impact analysis: which schema fields bind a given column / method / table), built by inverting the same per-field switch the forward producer uses so the two directions cannot disagree, holding no newBuildArtifacts/Workspacefield. Stage 3 (neighborhoodsubgraph tool) and indexing the forward-walkablePARTICIPATES(type → type) direction correctly deferred per R118 OQ6. Deviation from the spec’smcp/edges/sub-package: the edge model stays in the flatmcppackage to reuseMcpWire’s package-private grammar composers, single-sourcing the wire grammar rather than widening `McpWire’s deliberately-internal surface ; judged sound (it serves the same boundary-encoding principle D-D leans on). Coverage: MCP-handler-tier `GraphitronMcpServerTest(forwardColumn/ColumnReference-with-joinPath / table-boundServiceBacked/@node-type; reverse column / method / table directions asserting the endpoint slot holds the field not the queried node; ambiguous / notFound / two-overload fan-out reconciliation;Unavailable-before-build and memo-rebuild-on-build-swap), structured-content assertions only, plus unit-tierEdgeCoverageTestpartitioning every permit into edge-bearing / no-edge with overlap / missing / stale guards (the live drift-guard pin). In Review → Done reviewed by a session distinct from the implementer;graphitron-mcpmodule suite (52) and all 35 R374 tests green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(one full-reactor run flaked on the unrelated R372BgeEmbedderOnnxTeststrict-margin similarity assertion, which passes in isolation and on a focused module run; untouched by R374). -
R377 (
b44de9b; Spec → Ready2afdec9, Backlog → Spec212f21c, filed26e82a0):decode<typeId>mismatch when multiple@tabletypes share a table.BuildContext.resolveDecodeHelperForTableresolved throughfindGraphQLTypeForTable, an all-@tableindex that counts nesting-projection types and so returns empty (ambiguous) for any table backed by more than one object type; that routed decode resolution to a typeId-named fallback (decode<typeId>), which agrees withNodeIdEncoderClassGenerator’s emitted `decode<TypeName>only when typeId equals the type name. A customized numeric@node(typeId:)over such a table (a@nodeplus a nesting-projection@table, e.g.UTDANNINGSMULIGHET) emitted adecode<typeId>call javac could not resolve, a latent error that surfaced only in the consumer’s compile, found portingutdanningsregisteretto Graphitron 10. Fix: rewrite the resolver to the@node-onlyNodeIndexby-table view (nodes.forTable), which is exactly the right domain (it sees only@nodetypes, not the projection types sharing the rows), with a three-way outcome: one node → itsdecodeMethod()(type-name keyed, matching the encoder); two or more →null, which the four callers already map to a validate-time "zero or multiple GraphQL types map to it" rejection rather than a phantomdecode<typeId>(validator mirrors classifier invariants); no node → the orphan-input typeId fallback (synthesis-shim retirement track,retire-synthesis-shims.md). The wrong-domain branch 1 is dropped entirely rather than masked behind branch ordering;findGraphQLTypeForTablestays for its one remaining caller (the id-reference synthesis shim). The casing divergence is closed structurally:NodeIndex.byTableis keyed on the lowercased@table(name:)echo at construction (TypeBuilder.buildClassificationIndices) andNodeIndex.forTablelowercases its lookup arg, so a consumer never re-establishes theTableRef.sameTablecontract. Coverage: three pipeline-tierNodeIdPipelineTestcases (decode-via-index-not-typeId assertingdecodeSharedNodenotdecode10154; multi-node rejection toUnclassifiedTypewith the "zero or multiple" message; orphan-input typeId fallback pinning the branch-1-drop decision) over a newnodeidfixture.shared_nodetable with a customized numeric typeId, plus agraphitron-sakila-examplecompilation backstop (FilmEndorsementNode @node(typeId: "920534")overfilm_endorsement, already backed by theFilmEndorsementprojection, decoded by a newendorsementsByNodeIdquery) so the javac-stage failure is caught end-to-end by the module’s<release>17</release>compile; structural assertions only, no code-string assertions on generated bodies. In Review → Done reviewed by a session distinct from the implementer; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R375 (
8e0887acode,fe4303dplan-body test-name fixup; Spec → Ready0147443, Backlog → Spec4b443e9, filedb804f4c): Empty list passed to a fetch-path list-IN filter now narrows by nothing (DSL.noCondition()identity) instead of emittingIN (), which jOOQ renders as the constantfalseand silently zeroed the query. An external bug report (10.0.0-RC18, regression from 9.3.0) hit this through Apollo Client serialising an empty selection as[]on a list@nodeIdfilter argument: the empty list AND-ed an unsatisfiable predicate into theWHEREand dropped every row. The rewrite has nohasIdsbranch (R50/e4b collapsed list@nodeIdfilters without@conditiononto a plain column-shapedBodyParam.In/RowIn), so the symptom was general to every list-IN filter, not@nodeId-specific. Fix: a literal empty guard on all fourIn/RowInarms ofTypeConditionsGenerator.buildConditionMethod(non-nullif (!arg.isEmpty()); nullable folds the emptiness into its existing!= nullcheck);Eq/RowEqunchanged (scalars have no empty state). The fetch/lookup split is principled, not a per-field carve-out: on a fetch field a list filter is an optional narrowing predicate whose empty identity isnoCondition(the list-arity sibling of thenull/omitted case R230 already drops), while a lookup field’s input rows are the FROM-side of aVALUES…JOIN(LookupValuesJoinEmitter), where empty is an empty join domain and 0 rows is the only coherent answer;TypeConditionsGeneratoralready excludesLookupFieldupstream (line 63). The guard is emitted as a literal, not lifted into a sealedEmptyBehaviormodel slot: within this emitter "drop on empty" is a constant invariant with a single value, so the Generation-thinking two-consumer trigger does not fire and a single-case sub-taxonomy would be over-engineering (the DML-consumer lift point is named for if one ever appears). Scope item surfaced during implementation:filmsByNodeIdArg(argument-level same-table@nodeId, R106-lifted onto theWHERE film_id IN (…)rail) is a fetch field, so its empty and all-malformed execution tests carried stale pre-R106 lookup wording and asserted the empty set only becauseIN () = falsecoincidentally zeroed the query; both inverted to the unfiltered baseline. The all-malformed case (SkipMismatchedElementdrops every id → emptyList<Integer>, indistinguishable from a literal[]at the condition method) follows the samenoConditionrule per the wire-format-boundary principle (decode classifies skip-vs-throw at the boundary; downstream sees tuples with no provenance), and whether all-malformed should instead surface a user error is correctly split out to R378 (decode strictness, Backlog). Coverage: pipeline-tierTypeConditionsGeneratorTest(!ids.isEmpty()assertions on theInandRowInarms + newinFilter_nonNullList_emitsEmptyGuardWithoutNullCheckpinning the non-null arm the nodeId helpers don’t reach), execution-tierGraphQLQueryTest(invertedfilms_filteredBySameTableNodeId_emptyListReturnsUnfilteredBaseline, new connection regressionfilmsConnectionByOptionalIds_idsEmptyList_paginatesFullTableAndCountsAllasserting nodes +totalCountboth unfiltered, invertedfilmsByNodeIdArg_{emptyList,allMalformedIds}_returnsUnfilteredBaseline, and the lookup-divergence comment oninlineLookupTableField_emptyInput_returnsEmpty). In Review → Done reviewed by a session distinct from the implementer; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(GraphQLQueryTest 275, full suite 461 + 65, 0 failures). -
R376 (
64c6f23implementation,35a2e7estale-javadoc cleanup; Spec → Readyf3f2a26, spec refinementse60d49b52fc227, filed21c4e62): Goto-definition (and the declaration-name hover overlay) on a method-backed SDL field name now jumps to the bound Java method, not just the column / accessor / record-component cases that resolve through the enclosing type’s backing. A@service/@externalField/@tableMethodfield (and its root query/mutation forms) previously had no field-name jump at all: the cursor had to be parked on the directive’smethod:/className:argument beforeDefinitionswould navigate. The bound class+method were already resolved on the snapshot’sFieldClassification; goto and hover simply never consulted it for the name trigger. Routing: a newDeclTarget.methodBackedTargetarm consultsbuilt.fieldClassification(parentType, member)before theTypeBackingShapedispatch inofField, so a method-backed classification (six variants:ServiceBacked,Computed,TableMethod,QueryService,QueryTableMethod,MutationService) takes precedence over the parent table’s column backing; it rides the already-projected snapshot (no source-index read in the pure core), keeping the R371 goto/hover structural parity intact. Arity, primary with a name-level floor:DeclTarget.SourceMethodwidened from(class, accessorMethodName)to(class, methodName, paramCount)so both consumers key the source index on the same overload, retiring the hover overlay’s arity-0 hardcode (correct only for zero-arg POJO accessors; a service method takes at least aDSLContext, which would have made goto jump while hover returned empty, violatingoverlayIsPresentExactlyWhenGotoJumps).SourceWalker.Indexgains a never-droppedmethodsByNameview andresolveMethod: the precise(class, name, arity)key first (lands on the correct overload), falling back to the name-level view when that key is absent or was dropped as a same-arity collision, so a same-arity overload still lands on a declaration adjacent to the set rather than declining.DefinitionTarget.Ambiguousis retired: the directive-argDefinitions.methodTargetpath is aligned to the same floor (no Ambiguous non-jump remains anywhere on the navigation path, the spec’s open-question fold-in). Two documented, safe-degrading assumptions: the method-backed arm usesdefault → emptyover the 32-variantFieldClassificationrather than an exhaustive switch (a future method-backed variant silently won’t extend, scoped out by the spec forbidding aFieldClassificationmodel change), and keys the lookup on the resolved member name, relying on method-backed fields carrying no@field(name:)override (a miss degrades to the prior no-jump, never a wrong jump); for an arity-overloaded service name the classification records no signature, soresolve()takes the first catalog candidate’s arity and the name floor still guarantees a jump. Spansgraphitron-lspplus the smallSourceWalker.Indexaddition ingraphitron. Coverage:DeclarationDefinitionsTest(the four named variants jump end-to-end; an arity-distinguishablegreet()vsgreet(String,int)resolves to the correct overload, not the name floor; a same-arity collision still jumps via the floor; classified-but-unindexed returns empty),DefinitionsTest(the former Ambiguous case now falls back to a name-level jump),DeclarationHoverOverlayParityTest(a non-zero-aritySourceMethodparity case). In Review → Done reviewed by a session distinct from the implementer; full reactor green undermvn -f graphitron-rewrite/pom.xml install -Plocal-db(graphitron-lsp suite + 461 + 65, 0 failures). -
R372 (
c2acec1; Spec → Ready5dadff3, Backlog → Specc154ce2, filed47e4e24): MCP RAG foundation (R118 slice 8): the semantic-layer infrastructure thedocs.search(slice 9) andcatalog.search(slice 10) tools sit on, all module-local undergraphitron-mcp/…/rag/, registering no agent-facing tool and leavingGraphitronMcpServeruntouched. Three seams plus one lifecycle. D1: a graphitron-ownedEmbedderseam (embedQuery/embedDocuments/dimension) that owns the bge query/document asymmetry ;BgeEmbedderprepends the bge instruction prefix on the query path only ; and whoseQuery/Embeddingrecords bundle BM25 text with its KNN vector and name no langchain4j type, so the multilingual swap (R118 OQ2) attaches to this wrapper, not the library; English-onlybge-small-en-v1.5-q(384-dim) for V0. D2: anEmbeddingStoreseam withLuceneEmbeddingStore(BM25 + KNN in one index, fused by reciprocal-rank fusion) as the sole shipping backend, an in-RAMByteBuffersDirectoryinstance of the same class as the seam’s test fake, and the dimension invariant checked once ataddagainst the embedder’sdimension(). D3: a generic sealedWarmState<T>overWarming/Ready/Failedwith a handle-agnostic degradation-message helper (exhaustive switch, nodefault, rejectsReady), and theAsyncWarm<T>background-daemon harness whoseawait()returns the terminal value (neverWarming) so a dependent build-warm maps an upstreamFailedinto its ownFailedrather than hanging; volatile state read mirrors R361’s per-field posture. Dependency quarantine (R341): the one genuinely heavy native dependency (ONNX Runtime JNI, pulled transitively by the bge module) plus Lucene core land ongraphitron-mcpalone, never the plugin’s compile surface; surefire on this module gains--enable-native-access=ALL-UNNAMEDwith noexcludedGroups, so CI’s defaultmvn verify -Plocal-dbruns everything. Coverage: seam-tier (fast, no ONNX) asymmetry routing, store KNN round-trip + BM25 hybrid surfacing a lexical match, dimension guard, load-only-rejects-add,WarmStatetransitions across both type parameters + await propagation / cross-warm failure; infrastructure-tierBgeEmbedderOnnxTest(@Tag("slow"), runs in CI) loads the real bge model asserting dimension 384 and a strict-margin similarity separation. In Review → Done reviewed by a session distinct from the implementer; full reactor green under-Plocal-db(RAG seam + ONNX tests all run, 0 skipped). Blocks slices 9/10/11. -
R362 (
ea47993implementation; Spec → Ready173a7d0, Backlog → Spec304c39f, filedd7c8d15): MCPcatalog.tables/catalog.describeover a build-timeCatalogFactsprojection (R118 slice 2, on the R361 seam). Resolves R361 D1 to build-time enrichment (option A, not a retained loader): a new frozenCatalogFactsrecord (tables keyed by schema-qualified SQL name; columns with SQL+Java names, SQL types, nullability, comments; PK / unique keys; indexes; in/out FKs with their column pairs) is built once per catalog rebuild inCatalogBuilder.buildCatalogFacts(JooqCatalog)while the codegen loader is open, carried as a thirdBuildArtifactscomponent besideCompletionDataand the snapshot, and swapped onto avolatile Workspace.catalogFactsfield insetBuildOutput. The load-bearing invariant (noTable<?>/ForeignKey<?,?>/Field/Classretained) is what lets the projection outlive the per-passwithCodegenScopeloader close; new resolved-immutableJooqCatalogaccessors (allTableEntries,candidateKeys(Table<?>),columnFactsOf,indexFactsOf,foreignKeyFactsOf) reduce every live handle toStringat the parse boundary.GraphitronMcpServerregisters the two tools mirroring the R361statusToolshape:catalog.tables(schema + SQL-name-substring filters, opaque base64-offset cursor paging withnextCursor) andcatalog.describe(resolved / ambiguous / not-found arms over a parallelCatalogFacts.TableResolution).sqlTypemaps to the jOOQDataType.getTypeName()(SQL discovery key, not the Java FQN); no new classifier or validator branch (a read-only projection of already-classified facts). Coverage: pipeline-tier fact capture over the real Sakila catalog (film columns/PK/index/in+out FKs with column pairs,storage_binunique key distinct from PK) plus a structural recursive no-live-handle walk and a close-then-read smoke test; MCP handler tests driving a real loopback server assert mappedstructuredContentfor list/filter/page and describe resolved/ambiguous/not-found. In Review → Done reviewed by a session distinct from the implementer;mvn install -Plocal-dbgreen (CatalogFactsTest 9, GraphitronMcpServerTest 11, full suite 460 + 65). -
R365 (
3366bbefloor,976e0a3classifier+emitter,0e82f71execution fixture; In Review → Ready reworke7477ac, rework fixb28fe15, mutation fixture + drop-contractfd1f59e; Backlog → Specb6a6f93, Spec → Ready0426d42): Restore the graphitron 9.3 ability to return a polymorphic entity from a root@servicefield (route (a): the service hands back a PK-populated jOOQTableRecordper branch, and the generated fetcher dispatches on each returned record’s runtime class to pick the participant, tags__typename, and auto-fetches the selected columns by PK).ServiceDirectiveResolver.projectReturnType’s `PolymorphicReturnTypearm now resolves to a newResolved.Polymorphicsuccess (the all-@errorerrors-channel lift still takes precedence) instead of the old "not yet supported" reject; newQueryServicePolymorphicField/MutationServicePolymorphicFieldleaves carry the participant set and service method, andMultiTablePolymorphicEmitter.emitServiceMethodsreuses the multitable query path’s stage-2 by-PK auto-fetch (buildPerTypenameSelect) verbatim, replacing stage-1 UNION-ALL discovery with record-class dispatch over the returned records. Scope is exactly one shape: a@servicereturning a distinct-table multitable interface. Three guards keep that floor honest at the one sharedvalidateMultiTableParticipantssite and at classify: same-table participants in a plain multitable interface/union are anAUTHOR_ERROR(record-class dispatch cannot tell shared-recordClassparticipants apart, with or without@discriminator; model as a single-table discriminatedTableInterfaceTypeor split) ; this also guards R363’s query path; a@servicereturning a union is permanently unsupported (AUTHOR_ERROR, union polymorphism is a generated-query-path capability the service path never grew); a@servicereturning a single-table discriminated interface (TableInterfaceType) is deferred-rejected (the table-bound service path emits no per-row discriminator dispatch). Child@servicepolymorphic returns stay deferred (root only). The In Review pass requested rework on a silent-misdispatch hole (same-table participants carrying@discriminatorpassed the original no-discriminator-only floor and reached a dead-arminstanceofchain in the emitter, contradicting "validator mirrors classifier invariants"); the rework closed it by rejecting both same-table subsets and narrowing scope to interface-only. Route (b) (the{ field, errors }payload +errors:envelope shape) remains a separate follow-up on R366/R367. Coverage: pipeline-tier corpus (query-service-polymorphic,mutation-service-polymorphic) + a@ProjectionForprojection test, unit-tier floor pins inQueryInterfaceFieldValidationTest/QueryUnionFieldValidationTest(both same-table subsets →AUTHOR_ERROR), builder negatives (serviceReturningUnion_rejectedAsUnsupported,serviceReturningTableInterface_deferred), and execution-tierServicePolymorphicReturnExecutionTest(query single + list and a@servicemutation list round-trip, each dispatching two distinct-table branches by runtime record class against real PostgreSQL); the route (a) drop contract (a returned record matching no participant or no live PK row is dropped) is documented inbuildServiceMainFetcher. SharesMultiTablePolymorphicEmitterwith R363/R366/R367. In Review → Done reviewed by a session distinct from the implementer (the independent reviewer requested the rework and verified the green re-run).graphitron(full suite) andgraphitron-sakila-example(460 tests) green under-Plocal-db;graphitron-lsp/graphitron-mcpnot exercised at review (tree-sitter native runtime unavailable in the agent sandbox, orthogonal to this change which touches no LSP/MCP files). -
R367 (
208a3e5,629960b,162a7fd; Backlog → Specb6a6f93, Spec → Ready88e1659, In Review → Ready reworkcb2b4b9): Single-cardinality polymorphic child on a record-backed (Pojo / JavaRecord) parent, closing the capability gap the generator deferred atFieldBuilder’s `!fieldIsListarm (whoseRejection.deferred(planSlug: "polymorphic-child-record-parent-single-cardinality")pointed at a roadmap doc that never existed, a dead link in the generator’s own diagnostic).MultiTablePolymorphicEmitter.buildScalarPerParentFetchergains a record-parent arm: it now takesparentSourceKeyand, for aReader.AccessorCallparent, bindsparentRecordto the accessor’s returned hubTableRecord(Backing) env.getSource(.<accessor>()) instead of casting the source to a jOOQRecord(which wouldClassCastExceptionon a Pojo source); a null hub yields a null payload, and the table-backed arm keeps the(Record) env.getSource()cast.FieldBuilder.resolvePolymorphicRecordParentdrops the deferral so both cardinalities route throughderivePolymorphicHubSource, removing the only reference to the dangling slug. Scope is top-level backing classes; the nested-backing-classOuter$Nestednon-compiling-cast hazard (shared with the list arm viaClassName.bestGuessover a binary name) is filed as R370. Coverage: the two pipeline-tier deferral assertions inRecordParentMultiTablePolymorphicPipelineTestflip to assert successfulAccessorKeyedSingleclassification (interface + union), and execution-tierAddressOccupantCarrierSingleCardinalityTestdrives a Pojo carrier holding anAddressRecordhub throughQuery.addressOccupantCarrier, pinningfirstOccupantto the firstCustomer|Staffby sort order (Staff for a populated address) and to null over an occupant-free hub. The rework (162a7fd) fixed a red build the first In Review pass shipped: the no-occupants case queriedaddressOccupantCarrier(addressId: 4)butinit.sqlseeded only addresses 1-3, so a fresh DB returned a null carrier and the assertion failed (masked locally by a polluted persistent native DB; reproduced on a clean DB and in CI run 28104926051); the fix seeds an occupant-free address 4 (district Tasmania, no store/staff/customer) so the test exercises the empty-stage-1 null-payload arm over a non-null carrier rather than the null-carrier short-circuit, and corrects a stale SDL comment naming a nonexistent nestedAddressOccupantCarrierService.Carrier. Sibling of R366 (list cardinality); enables R365 shape (b); sharesMultiTablePolymorphicEmitterwith R363. In Review → Done reviewed by a session distinct from the implementer (independent reviewer requested the rework and verified the green re-run). Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 12 modules). -
R369 (
1a8b57d; Backlog → Spec87544d0, Spec → Readybf002a5, Ready → In Progressacef1bc):graphitron:devwalks generated-sources of scanned reactor modules so goto-definition / hover reaches jOOQ tables that live in a separate module from the schema module runningdev. Root cause was a lifecycle asymmetry between the two root setsAbstractRewriteMojofeeds the LSP:resolveClasspathRoots()readstarget/classes(on disk from any prior build, so every reactor sibling is scanned for completion), butresolveCompileSourceRoots()readproject.getCompileSourceRoots(), which only carries a generated-sources root once that module’s codegen plugin ran in this session, so a sibling jOOQ module unbuilt this session contributed zero walked source roots and its@table/@fieldjumps landed onDefinitionTarget.SourceAbsent(silent no-jump), breaking the R351 scan/walk parity invariant. Fix is in the shared resolver, not a dev-only branch (D2): newgeneratedSourceRoots(MavenProject)does a lifecycle-independent disk scan of the existing immediate subdirs oftarget/generated-sources/(thegenerated-sources/<tool>convention every generator follows, not POM-config parsing ; D1), andcompileSourceRootsOf(MavenProject)is the single per-module "what is walked" definition (getCompileSourceRoots()∪generatedSourceRoots) that bothresolveCompileSourceRoots()and the newunwalkedScannedModules(Iterable<MavenProject>)route through, so resolver and diagnostic cannot drift;collectExistingDirsdedups by normalised absolute path so a root the plugin already registered (full-lifecycle goals) collapses, making the widening a no-op there.DevMojorenders a startupWARNnaming any residual scanned-but-unwalked module (the dependency-JAR case with no.javato walk, explicitly out of scope for the auto-include). The staleresolveCompileSourceRoots()javadoc was rewritten to match. Coverage: unit-tierAbstractRewriteMojoTest(+5: disk discovery incl. stray-file exclusion, empty cases, the core widening regression, plugin-registered dedup,unwalkedScannedModulesreporting only the genuinely-unwalked module); unit-tierSourceWalkerTest.disjointGeneratorPackagesKeepTableJumpLocatedNotAmbiguouspins the D1 output-package-disjointness argument (a graphitron output root walked alongside a jOOQ root leaves the table-class jumpLocated, notAmbiguous) rather than leaving it as unpinned prose; no generated-body string assertions. Builds on R351 (parity invariant +collectExistingDirs) and R352 / R90 (SourceWalker.Index,DefinitionTargetempty-resolution contract). In Review → Done reviewed by a session distinct from the implementer.graphitronandgraphitron-maven-plugintest tiers green under-Plocal-db(AbstractRewriteMojoTest9/9,DevMojoTest6/6,SourceWalkerTest9/9); thegraphitron-lsp/DevServerTestcompletion-socket tests could not run in the agent sandbox (the tree-sitter native runtime install clones GitHub, which the egress policy denies ; orthogonal to this change, per the spec’s verification caveat). -
R361 (
d08b6ed; Backlog → Spec8d0f8e0, Spec → Ready60e0667): MCP shared-model seam, slice 1 of the R118 MCP programme. The R341 skeleton served static content only (promptscapability alone, an argument-lessaboutprompt) and held no reference to the live generator model. R361 widensGraphitronMcpServer’s constructor to `(InetSocketAddress, Workspace)and holds the live handle;DevMojo.bindServerpasses the sameWorkspaceinstance it hands the LSPDevServer, so the existing schema / classpath / source watchers refresh it in place with no new trigger, listener, or refresh path (thevolatilefields give per-field visibility on the next read). Thetoolscapability is declared (.tools(false), thelistChangedboolean) with one livenessstatustool that readsWorkspace.snapshot()on every call and reports the snapshot on its two orthogonal axes, availability (Built/Unavailable) and freshness (Current/Previous, absent when unavailable), mapped through an exhaustiveswitchover theLspSchemaSnapshotsealed permits with nodefaultso a new arm forces a compile-time choice rather than silently flattening; no domain counts (those are the later structured-tool slices' wire contracts). D1 keeps the seamWorkspace-only: the rawJooqCatalogis not threaded (it reflects lazily against thecodegenLoaderURLClassLoaderthatwithCodegenScopecloses each pass), deferred to slice 2. Adds the acyclicgraphitron-mcp→graphitron-lspcompile edge (plugin → {graphitron, lsp, mcp};mcp → lsp → graphitron), orthogonal to the module’s native-RAG dependency quarantine. Coverage: infrastructure-tierGraphitronMcpServerTestboots a real server and drives it with the real MCP client, assertingtools/listadvertises thestatustool andtools/callreturns the two-axis snapshot on both the defaultUnavailablearm and asetBuildOutput-drivenBuilt.Currentarm;DevMojoTest’s bind-failure unwind retargeted to the widened constructor; no code-string assertions. Builds on R341. In Review → Done reviewed by a session distinct from the implementer. Full reactor green under `-Plocal-db. -
R341 (
df99ed9; spec lifecycle115d174,56e8fb6,b271054,07755b5, Spec → Ready83f0542): MCP server skeleton embedded ingraphitron:dev. The smallest useful Model Context Protocol server that gives an MCP-aware agent (Claude Code, Cursor) ambient context about a graphitron project and, the load-bearing reason, establishes the transport-and-lifecycle seam the live catalog/schema discovery tools (R118) build on; it serves static content only. Newgraphitron-mcpmodule (GraphitronMcpServer implements AutoCloseable) hosts the MCP Java SDK 2.0.0 servlet-based Streamable HTTP transport (HttpServletStreamableServerTransportProvider) in embedded Jetty 12 EE10, bound loopback-only on127.0.0.1:8488(the LSP’s is8487) at the/mcpendpoint, serving the handshakeinstructionsstring plus a single argument-lessaboutprompt, both read once at startup from bundled jar resources (mcp/instructions.txt,mcp/about.md) mirroringLspVocabulary’s shape-not-state posture. The dedicated module is the dependency-quarantine seam that keeps R118’s heavy native deps off `graphitron-maven-plugin’s own compile surface (the "Separate business logic from API code" axis the `graphitron-lspsplit also serves, not transport symmetry); it is published likegraphitron-lsp(the plugin declares a compile-scope dependency and a Maven plugin resolves its declared deps from the consumer’s repositories at execution time), so it carries nomaven.deploy.skip. Lifecycle wiring:DevMojo.bindServerconstructs the server as a sibling of the LSPDevServerandDevMojo.cleanupcloses it;mcpPortdefaults toDEFAULT_MCP_PORT = 8488but is deliberately not a@Parameter(a configurable port stays deferred). A taken MCP port fails fast with aMojoExecutionExceptionnaming the conflict (the fail-fast diagnostic promoted out of Deferred for parity with the LSP bind, per "Stability through simplicity") and closes the already-bound LSP socket so a partial bind leaks nothing. The startup log names the MCP URL and a copy-pasteableclaude mcp addline;graphitron-sakila-exampleships a committed.mcp.json. Coverage (transport-glue layer, outside the four-tier classifier/emitter enforcement by design): infrastructure-tierGraphitronMcpServerTest(3, boots a real server on an ephemeral port and drives it with the SDK’s own client:initializecarries the bundled instructions;aboutis advertised argument-less and returns the explainer; a taken port throwsIOException);DevMojoTestgains the Mojo-message + no-LSP-leak case and aDEFAULT_MCP_PORT == 8488pin ondefaultsMatchPlanContract. Docs:getting-started.adocgains a user MCP subsection and the contributor "how this is wired" section grows from four to five cooperating components (+ mermaid node, Ctrl+C cleanup);README.adocmodule count 9 → 10 andgraphitron-mcpadded to the publishable surface. Out of scope and staying in R118: livecatalog/schematools over the warmWorkspace, docs RAG, any vector store / embeddings / ONNX, the stdio-to-HTTP proxy, and the decision on keeping R118’s heavy deps off the non-devplugin goals. In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 12 modules). -
R358 (
58bc29b,7202e20; spec61d5a9c, Spec → Ready2ac746d, revise Ready → Spec2ad3d45, Spec → Readyd75c838, Ready → In Progresse2e9612, In Progress → In Reviewe23b203): Guard table-name comparisons against case-sensitivity drift.TableRef.tableName()is the case-preserved verbatim@table(name:)echo, so the same logical table can surface as two differently-cased strings (the verbatim@tablecasing vs the lowercase jOOQTable.getName()the record-class resolution path feeds in), and a case-sensitive.equalssilently mis-decides under an Oracle-style UPPERCASE@tableover a lowercase jOOQ catalog: the R357 bug, whichFieldBuilder.resolveCarrierIdEncoder(:3105) was one explicit@nodeId(typeName:)hop from reproducing. Two phases. Phase 1 (58bc29b): convert the:3105@nodeId(typeName:)NodeType-vs-carrier comparison to case-insensitive, and addTableNameComparisonCaseGuardTest(@UnitTier), a recursive source scan oversrc/main/java/no/sikt/graphitron/rewriteforbidding the.tableName().equals(spelling with a nonzero-scanned-file tripwire against a vacuous pass. Phase 2 (7202e20): move the comparison onto the type asTableRef.sameTable(String)/denotesSameTableAs(TableRef)(case-insensitive, null-safe canonical identity;tableName()stays the verbatim diagnostic echo), migrate all ~10 comparison sites acrossFieldBuilder/TypeBuilder/GraphitronSchemaValidator/BuildContext/NodeIdLeafResolver(both operand orientations, behaviour-preserving), and strengthen the guard to forbid every rawtableName()comparison (both.equals/.equalsIgnoreCase, both orientations) excluding the predicate’s homemodel/TableRef.java. The guard is then a backstop on a predicate correct by construction, the "model carries what the consumer needs" principle (rewrite-design-principles.adoc:17: the same predicate evaluated by multiple consumers is a sign the resolver is under-specified). Coverage: pipeline-tierMutationDmlNodeIdClassificationTest#bulkDeleteIdCarrier_explicitNodeId_caseMismatchedTable_admits(explicit@nodeId(typeName: "Bar")with UPPERCASE@table(name: "BAR")NodeType over lowercase@table(name: "bar")carrier; the spec’s:3105reachability obligation, option (b)), verified rejecting pre-fix (the carrier drops to a non-SingleRecordIdFieldFromReturningclassification) and admitting post-fix (encodeBarwired, empty diagnostics); unit-tierTableRefSameTablePredicateTest(matching/mismatched casing both directions, null arg); the strengthenedTableNameComparisonCaseGuardTestas the structural net, verified tripping on a planted raw comparison with a named-site message; no generated-body string assertions. Out of scope and filed as R359 (d1985ec,column-sqlname-comparison-case-guard): the structurally identicalColumnRef.sqlName()sibling (one live.sqlName().equals(atGraphitronSchemaValidator.java:883alongside sixequalsIgnoreCasesites). CanonicalizingtableName()at construction rejected because it would change author-facing diagnostic casing (a documented invariant). Depends-on R357 (which convertsFieldBuilder.java:5114; R358 owns:3105and the structural pin). In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db). -
R357 (
faa465b; roadmap add3b7d54a, Backlog → Spec8d8cb45, Spec → Readyfdad51d): Case-insensitive@table(name:)match in record-composite carrier accessor resolution. A@servicerecord-composite carrier (R329’s two-levelPayload { results: [Result], errors }/Result { @table children }shape) whose result-type@tablechildren declare@table(name:)in a case differing from the lowercase jOOQ catalog name misclassified every such child asUnclassifiedFieldwith the three-optionresolveRecordParentSourceauthor error.collectAccessorMatches(FieldBuilder) grounds the DTO and resolves the accessor plus its element table correctly, then dropped the match on a single case-sensitive element-table guard:expectedSqlNamecarries the verbatim@table(name:)casing (viaresolveTable) while the accessor’s elementTableRefcarries the jOOQTable.getName()casing (viaresolveTableByRecordClass), and the two diverge only when SDL casing differs from catalog casing (the driving utdanningsregisteret schema writes UPPERCASE@table(name:)against lowercase Postgres). The one-line fix aligns that comparison toequalsIgnoreCase, the table-name idiom already used at eight other sites. Surfaced by the utdanningsregisteret Graphitron 10 migration. Coverage: pipeline-tierServiceRecordCompositeCarrierPipelineTest#caseMismatchedTableName_classifiesCompositeChildrenAsRecordTableField(the R329 FilmWithActors carrier with@table(name: "FILM")/@table(name: "ACTOR")against lowercasefilm/actor, asserting both children classify asRecordTableFieldONE/MANY with empty diagnostics, the verdict not the case-insensitivity mechanism), verified failing pre-fix (both fall toUnclassifiedField) and passing post-fix; no generated-body string assertions. Out of scope and filed as R358 (table-name-comparison-case-guard): the sibling.tableName().equals(atFieldBuilder.java:3105(R358 carries the conversion and is re-examining whether it is a latent instance of the same bug rather than inert) and a unit-tier guard pinning the idiom; canonicalizingTableRef.tableName()rejected because it would change author-facing diagnostic casing (a documented invariant). In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R351 (
bdabb1e,ffb2589; In Progress → In Review59e1cc3; absorbs R352): Complete the LSP goto-definition decoupling R349 began, so the jOOQ half rides the source-cadence index too and both halves resolve through one shape. Source-root parity made structural:AbstractRewriteMojo’s scan path (`resolveClasspathRoots) and walk path (resolveCompileSourceRoots) collapse onto one package-private, unit-testedcollectExistingDirstraversal over the same reactor project set, so a class scanned for completion provably has its source root walked for goto-definition;DevMojologs a startup classpath-root / source-root / external-reference count so the "completion works but goto-definition returns nothing" report self-diagnoses. jOOQ half on the source index:CompletionData.Table/Column/ReferencedropSourceLocationand instead carry the generated table-class FQN (and theKeys-class FQN for references);Definitionsjoins those FQNs against the LSP-ownedSourceWalker.Indexat request time and routes the@table/@field/@referencearms through the same exhaustiveDefinitionTargetswitch the service half uses, retiring the file-head0:0synthesis (a known table whose source is not on a walked root lands onSourceAbsent, a clean non-jump). Hover /descriptiononto the source cadence:CatalogBuilderno longer walks.javaat all (descriptions are the build-derivable fallback only: the jOOQ table’s SQL comment, empty for columns and services); a newDescriptionsoverlay reads the source-derived Javadoc from the index at request time with per-element precedence (table SQL-comment wins; column / class / method source Javadoc wins), andHoversplus theFieldCompletions/TableCompletionsdetail read through it, so hover and goto cannot show two snapshots of one declaration mid-edit. Static cache → instance:SourceWalker’s per-file cache moves from a process-wide static onto an instance owned by `Workspacealongside thevolatile sourceIndex;Workspace.refreshSourceIndexis the single walk entry point, called by the dev goal’s source-root watcher. Coverage favours real end-to-end over mocks:SourceCadenceHoverAndDefinitionTestwalks real.javathrough the realWorkspace/SourceWalker/Hovers/Definitions(asserting hover and goto move together across a source edit with the catalog the same instance, no rebuild);CatalogBuilderSourceTestinverts to pin the build-boundary decoupling (documented sources on the build are not lifted);SourceWalkerTestpins per-instance cache isolation (a same-mtime content change a static path+mtime cache would mis-serve);AbstractRewriteMojoTestpins thecollectExistingDirsclasspath/source-root parity;DefinitionsTestreaches every jOOQ arm and everyDefinitionTargetoutcome; no code-string assertions on generated method bodies. In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db). Predecessor R349 (the service half and theDefinitionTargettyping); absorbed R352 (the jOOQ-half / hover-cadence / static-cache follow-up R349 deferred). -
R342 (
fa2743f; spece736a03, Spec → Readyde7490b): Structural dedup + value agreement for bulk UPDATE SET columns written by overlapping carriers. The fourth and last mutation write surface left open by R322 (the bulkUPDATE t SET c = v.c FROM (VALUES …) AS v(…)path) and the lift of R354’s deferred bulk self-FK form. The bulk path walked its SET groups per-group with no cross-group column dedup, so two writers landing on one backing column emitted that column twice in the derived table’sv(…)list and crashed loud (a duplicate-column Postgres/jOOQ error, not a silent drop, which is why R322 deferred it here). Two shapes were affected: a decode-involving within-SET overlap (a plain@fieldplus a@nodeIdFK reference whose lifted child column coincides), and a self-FK@nodeIdon a list-input UPDATE (rejected at validate time byUpdateRowsWalkerStage 2b). Plan:TypeFetcherGenerator.setColumnPlan(List<SetGroup>), the SET analogue ofinsertColumnPlan, groups the set groups' columns by backing-columnsqlNameinto an ordered writer list (each carrying its sourceSetGroupindex, slot, andColumnRef),shared()when ≥2 writers; the three bulk emitters (emitSetVColNameAdds,emitSetBulkCellAdds,emitSetVFieldPuts) all walk the one deterministic plan, so thev(…)column-name list, the per-row cells, and thesets.putentries emit exactly one entry per distinct column and cannot drift out of positional alignment. Presence gate (setColumnPresenceGate): a disjoint column keeps its single writer’s first-row gate byte-identical to the pre-dedup form; a shared column’s gate is the disjunction of its contributing writers' first-row presence, and the uniform-shape guard makes projecting row 0’s disjunction onto every row safe (a gate keyed on the wrong writer would silently drop a present shared column, the exact silent-drop class R322 closed on the other surfaces; pinned by the two asymmetric-presence execution tests). Cells: the per-row decode locals are hoisted once per row intoemitBulkSetDecodeLocals(INSERT-styleinstanceof Stringguard + presence-gated throw), so a composite group’s cells and a shared column’s gather all read one decode rather than re-decoding per writer; a shared column gathers the present writers' values (reusing R354’s presence-guardedappendAgreementValue), pairwise-checks them throughNodeIdEncoder.requireColumnAgreement, and adds the single coalescedDSL.val(firstPresent, col.getDataType())cell,emitInsertAgreementPrep’s coalesced-cell shape transplanted into the row loop (no `DSL.defaultValuebranch, since the conditional gate guarantees a present writer, and notemitSetAgreementPreamble’s check-then-let-the-puts-run shape, which the bulk derived table’s lack of a last-write-wins `Map.putaffordance forbids). Cross-partition (WHERE∩SET self-FK) fork (resolved in In Progress withprinciples-architect):setColumnPlansees only SET groups, so a self-FK’s column shared with the WHERE identity is handled outside it, the two v-populating emitters skip a SET column already supplied as a WHERE/lookup v-column,emitSetVFieldPutskeeps the no-opsets.put(keepssetsnon-empty so the empty-SET runtime guard does not fire on a minimal self-FK input), and a newemitBulkKeySetAgreementemits the per-row check reusing the already-present per-row decode locals (bulkKey<gi>WHERE-side,bulkSetKey_<gi>SET-side) rather than re-decoding (two decodes/row, not three; the agreement guards the values actually used). Walker: Stage 2b’s bulk self-FKUnsupportedInputFieldShapereject is deleted and the now-deadlistparameter dropped fromwalk(bothFieldBuildercall sites + the class javadoc updated); the walk is cardinality-independent, since R354 already routed a self-FK all-SET regardless of the list flag, so removing Stage 2b exposes a shape the classifier already routes correctly.UnsupportedInputFieldShaperetains its other producers. Coverage:UpdateRowsWalkerTest(15 pass: the bulk self-FK reject test inverted to admit-and-route-all-SET with the sharedmailbox_idin both partitions, plus a decode-involving-overlap-admits-without-PlainColumnCollisiontest); execution-tierBulkUpdateSetAgreementExecutionTest(7 tests: within-SET agree / disagree-rolls-back / asymmetric-present ×2 on thefilm_endorsementupdateEndorsementsOverlapfixture, self-FK agree-repoints-with-mailbox_id-no-op / disagree-rolls-back / omitted-nullable on theemailupdateEmailRepliesfixture), the list-input siblings ofNodeIdValueAgreementExecutionTest/SelfFkNodeIdUpdateExecutionTest; compilation tier via the two new schema fixtures against real jOOQ at Java 17; no generated-body string assertions. Out of scope: the shared overlap-analysis abstraction lift across the now-six instantiation sites (R356, filed alongside this spec, depends on it); non-Postgres dialects; any change to the single-row UPDATE SET / INSERT /@servicepaths (R322/R354). In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R355 (
6562b7a; Backlog → Spec8133ecf, Spec → Ready42efe84): Infer depth-1 nested@conditionarg bindings by name withoutargMapping. A@conditionwhose slot is an input object with scalar fields previously required an explicitargMappingto bind the condition method’s parameters to the nested fields, even when the parameter names already matched the nested field names exactly (the motivatingsearchVektingstallRange(table, fra, til)againstSokVerdiRange { fra, til }); R355 drops that boilerplate. Core inference (ServiceCatalog.inferBindingsByType): a new name-keyed depth-1 branch runs on the parameters still unbound after the arity-unique and type-unique branches. For a parameter whose name matches exactly one direct field of a single unclaimed input-object slot, by name ANDmapToJavaTypeName(so only canonical-scalar-typed leaves, never a named-input-object / enum / unclassified-scalar leaf the emitter cannot vouch for, the same null-is-no-match discipline theunambiguousReachablePathsuggestion uses), it binds the parameter one level in viainferNestedFieldByName→PathExpr.step(head(slot), field, liftsList). The synthesisedPathExpris byte-identical to the one a hand-writtenargMapping: "p: slot.field"produces;liftsListis computed viaArgBindingMap.isListShaped(relaxed private → package-visible) so a list-shaped leaf is not hardcoded. Zero or >1 candidates leave the parameter unbound, so the existing per-parameter rejection /argMappingsuggestion still fires; ranging over the unclaimed slot set keeps it a peer of its siblings and is what makes an ambiguous name fall through. Depth ≥ 2 stays explicitargMappingby design (deeper descent drags in recursive input types and path-dependent uniqueness; one hop is where the name still plainly names its source field). The disambiguator is the parameter name, orthogonal to R219’s count axis, so it lands as a distinct branch. Emit deviation (ConditionResolver.rewrapForNested): the spec’s "Files in play" said no emit change was expected (thePathExprequals the explicit-argMappingone), but the execution test surfaced a latent gap shared by both forms: the input-field-@conditionrewrap folded only the walk’s path to the input field and dropped each parameter’s own descent, so a multi-segment binding cast the whole wrapperMapto the leaf type (a defensive-cast-that-throws, against "classifier guarantees shape emitter assumptions"). A newnestedPathhelper now appends the per-parameter path tail (segments after the head, the head naming the input field already atleafPath’s tail) to the `NestedInputFieldpath; a bare-head (single-segment) binding returnsleafPathunchanged, byte-identical for every pre-R355 binding, and the fix also completes the explicit-argMappingform for the same shape. The R214ServiceCatalogTestcase that deliberately yielded "to name-based matching" now asserts the concrete inferred binding (input.filmId) R355 produces instead of the suggestion it used to print, the intended R214 → R355 handoff (a strengthened assertion, and the type-unique yield it relies on stays implicitly exercised). Coverage: unitServiceCatalogTest#inferNestedFieldByName_*(single scalar match, listliftsList=true, name-with-wrong-type → null, two-slot ambiguity → null); pipelineGraphitronSchemaBuilderTest(INFER_NESTED_CONDITION_ARG_BY_NAMEasserting the inferredPathExprchain, its*_EXPLICIT_ARGMAPPING_EQUIVALENTsibling asserting the identical values to pin equality,*_LIST_LIFTS_LISTpinning the computedliftsList=true,*_AMBIGUOUS_FALLS_THROUGHpinning the rejection, all on classifier-outputPathExprvalues not generated-body strings); executionGraphQLQueryTest#inputFieldCondition_nestedArgInferredByName_filtersSameAsExplicitArgMapping(arental_rate[2.0, 3.0]round-trip selecting exactly the three 2.99 films, proving both bounds bind to the right nested field, asserted identical to the explicit-argMappingsibling). In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). Related: R249 (nestedargMappingsyntax, the explicit axis), R219 (unify arity-/type-unique under oneJavaTypeKeycount rule, the type-based inference this sits beside), R214 (the layered type-based inference this follows). -
R329 (
5b59e4a; spec268e404, revised21a0d36, Spec → Ready8c414b6): Re-admit@servicecarrier payloads with a record-composite data field, landing R75 Phase 3 on the post-R276 reflection-driven binding model. An@servicemutation whose method returns a list (or single) of a consumer-authored composite (a POJO bundling several jOOQ records, e.g. oneFilmRecordplus aList<ActorRecord>) is now expressible as a two-level carrier: a payload whose non-@tableobject data field is a list of an intermediate result type whose@field-mapped@tablechildren map onto the composite’s components. Previously this dangled (rejectDanglingTypeReferences) ; the result axis skipped the wrapper under the cardinality-match guard, and the carrier axis admitted only@table-typed data fields. Binding side: the cardinality decision is lifted into one builder-internal sealedProducerBindLevel { BindsWrapper, BindsDataFieldElement, NoBind }read by both the single-level and two-level paths (theArgumentRef-style classify-once/project-into-each-consumer pattern), replacing the oldsdlIsList != reflectedIsMultireject + carrier-admit pair that would desync into a dangle or double-bind;BindsDataFieldElementgrounds the data field’s element type to the producer’s reflected return-element on the existing result axis, so no newProducerBindingarm and no nullable-TableRefgeneralization ofServiceEmitted. Carrier recognition:carrierTableBindingbecomes a sealedCarrierBinding { TableBacked, ClassBacked, NotACarrier }with onecarrierVerdictprojection shared bylookAheadVerdict, the producing-edge registration, and the nesting/orphan guards, so the verdict cannot drift; the composite carrier classifies as a class-backedResultTypenaming the per-element composite, gated on the payload being@service-produced (BuildContext.isServiceProducedPayload, the single producer of that fact, shared with the errorsWrapperArmselector) and not itself result-axis-bound. Emit side: a newChildField.RecordCompositeFieldleaf, justified by a dimensional row (Record/bareFetch/listOrSingle(Record)/Plain(composite)) distinct fromRecordField(Field target),RecordTableField(Table target), andServiceRecordField(ServiceCall operation); a source-passthrough projection carrying its ownSourceEnvelope(DIRECT/OUTCOME_SUCCESS) rather than recomputing it at emit, reusing the existingOutcomeWrapperArmfor errors withcomputeMutationServiceRecordReturnTypere-levelled to the reflected method return. Validator mirror: the three near-misses surface through existing recognizers (mismatched producer →RecordBindingMultiProducer; a@fieldchild neither@table-backed nor a resolvable composite accessor → accessor-mismatch; the re-levelled cardinality mismatch →checkServiceReturnMatchesPayload), not new predicates kept complementary by hope. Coverage: aClassifiedCorpusentry pinning the payload classification + data-field verdict, a@ProjectionFor(RecordCompositeField)LSP-projection assertion,RecordCompositeFieldinIMPLEMENTED_LEAVES(GeneratorCoverageTeststays exhaustive), pipeline-tier positive coverage (ServiceRecordCompositeCarrierPipelineTest: list + single arrival,DIRECT/OUTCOME_SUCCESSenvelope, errorsWrapperArm,@tablechildren asRecordTableField), validator-tier coverage of the three near-misses (ServiceRecordCompositeCarrierValidationTest), and an execution-tier round-trip ingraphitron-sakila-example(GraphQLQueryTest:List<composite>projection + the error arm renderingdata: null) backed by the cross-module compile against real jOOQ records; no generated-body string assertions. Surfaced by the utdanningsregisteret Graphitron 10 upgrade (opprettUtdanningsspesifikasjonOgUtdanningsmulighet). The batch-keyedMap<Key, List<composite>>shape stays out of scope for root carriers: a root@servicereturns the composite list directly, whilepeelReturnElement/isMultiCardinalityReturnalready peelMapfor the orthogonal child-batching path and aMapreturn at a root carrier rejects loudly viacheckServiceReturnMatchesPayload(settled, not half-wired). In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R354 (
46522ba; spec64913b8, Spec → Ready8e46e7f): Self-FK@nodeIdon a Graphitron-owned single-row UPDATE routes all-SET with a cross-partition value-agreement check. The UPDATE sibling of R328 (which shipped the self-FK@nodeId @referenceon the INSERT/read sides and theemail/mailboxfixture). On@mutation(typeName: UPDATE)a self-FK whose child columns straddle the row’s identity key (email_in_reply_to_fk’s `(mailbox_id, in_reply_to_no), wheremailbox_idis a PK member) previously trippedUpdateRowsError.MixedCarrierKeyMembership: the straddle check partitions at input-field granularity, so a self-FK overlapping the PK had no expressible UPDATE form. Design: a self-FK reference is a write of "who this row points at" (its parent), never the row’s own identity, so it routes its lifted columns wholly to SET regardless of key membership; the shared key column then appears in both the WHERE (from the identity field) and the SET (from the self-FK), ordinary SQL whose two decoded values the FK constraint forces equal. Marker: aselfReferenceboolean onInputField.ColumnReferenceField/CompositeColumnReferenceField, set once at theNodeIdLeafResolverdiscrimination site (whereT.table()equals the containing table) and threaded throughBuildContext; every other construction site passesfalse(the fact-lives-in-the-model lift, the walker readscarrier.selfReference()rather than re-deriving self-ness). ThreeUpdateRowsWalkersites fork on it: Stage 6 routes all-SET, Stage 4-5 computes key coverage over the non-self-FK columns only (a PK column reachable only via the self-FK correctly failsNoUniqueKeyCoverage, a self-FK cannot pin the row it lives on), and Stage 2b defers a self-FK on a bulk (list-input) UPDATE to R342 with a clearUnsupportedInputFieldShapereject rather than a silently-wrongFROM (VALUES …)derived table. The narrowedMixedCarrierKeyMembershipstill rejects a genuine cross-table FK straddle (a cross-table FK’s lifted column can legitimately be the row’s own identity, so it keeps partitioning by membership;fkTargetNodeIdRef_arity1_update_admittedpins that the all-SET rule did not leak). Emit:TypeFetcherGenerator.emitKeySetAgreementPreambleadds a cross-partition (WHERE∩SET) agreement preamble in the single-row UPDATE arm, decoding each side into a presence-guarded preamble-local and passing both through R322’srequireColumnAgreement(reused unchanged) before the DML; the throw names both contributing input fields. A second WHERE predicate was rejected as a silent drop wearing a no-match costume. This is the WHERE↔SET boundary R322’s four same-clause agreement sites never crossed, landed as a deliberate fifth instantiation of the gather-and-compare scaffold (theemitAgreementDecodeLocal/appendAgreementValuehelpers are the seam, the carrier-agnostic writer-abstraction lift stays R342's). Coverage:UpdateRowsWalkerTest(all-SET routing, coverage-via-self-FK →NoUniqueKeyCoverage, bulk reject, cross-FK straddle stillMixedCarrierKeyMembership),MutationDmlNodeIdClassificationTest(updateEmailReplyclassification + the marker on both carriers),graphitron-sakila-examplecompilation ofUpdateEmailReplyInputagainst real jOOQ at Java 17, and execution-tierSelfFkNodeIdUpdateExecutionTest(agree repointsin_reply_to_nowith themailbox_idSET a no-op / disagree throws and rolls back, no silent row-move / omitted nullable updatessubjectonly, no agreement check); no generated-body string assertions.docs/typed-rejection.adocnarrowed andRejectionSeverityCoverageTestannotated to reflect the self-FK no longer reaching the straddle arm. In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R349 (
1db8756): Decouple service-half LSP goto-definition positions from the generator build and type the resolution outcome. Goto-definition on a@service/@condition/@externalFieldclass or method reference silently no-opped in a livegraphitron:devsession when the reference’s source root was scanned for bytecode (so completion worked) but not walked for positions:CatalogBuilder.enrichExternalReferencesleft theCompletionData.SourceLocationat theUNKNOWNsentinel (uri="",line=0), whichDefinitionscollapsed to "no jump" through oneuri().isEmpty()test, making the recoverable not-yet-indexed case indistinguishable from the two correct no-ops (binary-only source genuinely absent; overload-ambiguous). Change 1, typed outcome: a new sealedDefinitionTarget { Located, SourceAbsent, Ambiguous }(ingraphitron-lsp/…/definition/) replaces the sentinel;Definitionsresolves the service half through two pure FQN-join helpers (classTarget/methodTarget) and switches on the typed outcome exhaustively in one place (Definitions.resolve), whereSourceAbsentlogs a recoverable signal andAmbiguousis a deliberate silent no-jump.SourceWalker.Indexnow exposes theambiguousMethodsset the merge already computed and discarded, soAmbiguousis distinguishable fromSourceAbsent. Change 2, source-cadence index:CompletionData.ExternalReference/Methoddrop theirdefinitionfield; the LSP owns avolatile SourceWalker.IndexonWorkspace(sourceIndex()/setSourceIndex), seeded byDevMojoat startup and refreshed by a third watcher (sibling to the schema and classpath watchers) on.javaover the compile source roots, so a declaration that moves in a hand-edited source is jumpable without waiting for a.classrebuild.Definitionsjoins ref-from-catalog with position-from-source-index at request time on the FQN both carry.CatalogBuilderkeeps lifting Javadoc intodescriptionon the build cadence (hover untouched). Scope bounded to the service half (the reported bug); the jOOQ half, hover-description cadence, and the staticSourceWalker.CACHEare deliberate transitional states, each documented and deferred to the follow-up R352 (complete-lsp-position-decoupling). Coverage:DefinitionsTestpins eachDefinitionTargetarm reachable (Located/SourceAbsent/Ambiguous) plus the end-to-end class/method jumps;SourceWalkerTestpins theambiguousMethodsexposure;CatalogBuilderSourceTestasserts the build-cadence Javadoc lift now that positions are LSP-tier;CatalogRefreshTestpins a.javaedit refreshing the source index without a catalog rebuild; no code-string assertions on generated method bodies. Predecessors R90 (the source walk) and R351 (thecompileSourceRoots/classpathRootsparity stopgap). In Review → Done reviewed by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db). -
R350 (
8c88f5d): Workspace-wide LSP goto-definition for GraphQL type references. Goto-definition on an intra-schema type reference (theFilminfilms: [Film!]!, animplementsinterface, or a union member) previously resolved only when the file declaring the target type was in an open buffer; in a real multi-file schema the declaration is frequently not open, so the jump silently no-opped ([]) even though theTypeDefinitionRegistryalready holds every type’s source position and ships to the LSP through the build snapshot. Producer:LspSchemaSnapshot.Builtgains a per-type declaration-location map (typeDefinitionLocations(), keyed by SDL type name) plus atypeDefinitionLocation(name)lookup, threaded through both leaf records' canonical constructors andWorkspace.demoteSnapshot; a new 5-arg convenience constructor on each ofCurrent/Previousdefaults the map empty so existing fixtures compile untouched.CatalogBuilder.buildSnapshotpopulates it fromregistry.types()+registry.scalars(), reducing graphql-java’s 1-basedSourceLocationto the 0-based coordinates every goto-definition consumer reads (mirroringSourceWalker’s `-1); null-source built-in scalars and the bundled-directive source are dropped rather than emitted as deadfile://URIs, the bundled source-name exposed asRewriteSchemaLoader.DIRECTIVES_SOURCE_NAME. Consumer:IntraSchemaDefinitions.computetakes the snapshot as an explicit parameter (test seam matching theDefinitions.computesibling convention, since the provider’s onlyWorkspace-snapshot install path demands fullBuildArtifacts); the open-buffer tree-sitter scan stays first and authoritative, falling back tobuilt.typeDefinitionLocation(typeName)on miss, andGraphitronTextDocumentServicepassesworkspace.snapshot()at the call site. Coverage:IntraSchemaDefinitionTest(the three acceptance arms: snapshot fallback when the declaring file is not open, open-buffer precedence over a deliberately stale snapshot entry, neither-source no-op) andCatalogBuilderSnapshotTest(a producer-side arm over the realRewriteSchemaLoader.loadparse path asserting user types/scalars land at 0-based positions while built-ins and bundled-directive inputs/enums are dropped); no code-string assertions on generated method bodies. Out of scope: the pre-existing 1-basedsourceLocation(GraphQLScalarType)completion-feed helper (a separate code path, not this fallback). In Review → Done approved by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules incl.graphitron-lsp). -
R328 (
27d2359): Self-FK@nodeIdreference on Graphitron-owned DML mutation inputs. A same-table@nodeIdcarrying an explicit@reference(path: [{key: …}])now means "follow this self-FK and write its child columns" instead of "use my own identity", read/write symmetric and the sibling of R315 (cross-table FK-reference@nodeId) and R322 (the shared-column dedup + agreement it rides on). D1 gates theNodeIdLeafResolver.resolvesame-table own-PK short-circuit on@referencebeing absent; with@referencepresent the leaf falls through toresolveFkJoinPath, which orients the self-FK withselfRefFkOnSource=trueand yields aResolved.FkTarget.DirectFkwhoseliftedSourceColumnsare the self-FK’s child columns on the row’s own table, the same data shape a cross-table FK carries, so no new sealed variant. The single shared gate lives inresolve(), so the shape is admitted on the read side too (a same-table@nodeId @referencequery arg / filter resolves to aDirectFkself-FK filter,WHERE child_cols IN (decoded keys), no self-join, and the@asConnectionsame-table advisory correctly stops firing). D2 lifts the R315InputBeanResolver.buildRecordKeyDecodesame-table "self-reference out of scope" reject, routing the@servicejOOQ-record case through the sameBuildContext.resolveRecordFkTargetColumnsthe cross-table branch uses (orientedselfRefFkOnSource=true), landing the decode on the self-FK child columns, never the record’s own PK. D3 adds no emitter code: R322’s per-column structural dedup +requireColumnAgreementcarry the shared-column overlap (the natural CAMPUS/email case where the self-FK’s first child column coincides with a cross-table FK’s), proven end to end. D4 pins cross-path consistency by test rather than a new abstraction: thesame-table && !@reference ⇒ self-FKpredicate plus the node-key reconciliation are duplicated across the classifier (permutationToKeyColumns) and record-population (resolveRecordFkTargetColumns) paths, so anti-drift tests assert both land identical child columns on the identity permutation (email fixture) and off it (the existingreordered_fk_childfixture forcing a non-identity permutation through both reconciliations). Coverage: resolver-tierNodeIdLeafResolverTest(DirectFk landing + without-@referenceidentity contrast + two anti-drift cases),MutationDmlNodeIdClassificationTest(INSERT admits theCompositeColumnReferenceFieldover the self-FK child columns, surfacing the sharedmailbox_id),JooqRecordServiceParamPipelineTest(@serviceclassifier, replacing the dropped reject test),SelfFkNodeIdReferenceReadSidePipelineTest(read-sideDirectFkfilter + no-@asConnection-advisory), and execution-tierSelfFkNodeIdInsertExecutionTest(agree inserts / disagree throws + inserts nothing / omitted nullable leaves the lone decode, on theemail/mailboxfixture, jOOQ schema 1.8 → 1.9); no generated-body string assertions. Out of scope: the bulk UPDATE SET decode-overlap dedup, the one surface where a self-FK shared-column overlap still fails loud (a duplicate derived-table column), owned by R342 (Backlog); R328 only makes that gap reachable via a natural self-FK shape rather than a contrived one. In Review → Done reviewed by a session distinct from the implementer (27d2359). Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R322 (
42ac9b2@servicejOOQ-record path D3+D4;0535abb@mutationINSERT path D1/D2/D5;f739fd5single-row UPDATE SET D3 + UPDATE-path D2;0a9c8dfIn Review markup + R342 filing): Runtime value-agreement check for multiple@nodeIddecodes onto shared columns. When more than one writer (two@nodeIddecodes, a@nodeIddecode plus a plain@field, or two composite FKs) lands on a single backing column, agreeing values are harmless but a disagreement would silently overwrite a caller-supplied value, the "no silent drops" failure the generator exists to avoid; the disagreement is only observable at runtime (values arrive off the wire), so it cannot be a build-time reject. D3 adds the shared predicateNodeIdEncoder.requireColumnAgreement(label, DataType, a, b): it coerces both sides through the destination column’s jOOQDataType(the same coercion the real write applies, riding the class-level@SuppressWarningsforDataType.convert, so format-variant wire values like"01"/1.0/BigInteger 1collapse onto the decoded1and agree, while a genuinelyvarchar"01"vs"1"still disagrees) and throwsGraphqlErrorExceptionon disagreement; one home for the message and semantics so the paths cannot drift. D1 resolves, per backing column, the ordered list of contributing writers, consumed by D4 (the@serviceJooqRecordInstantiationEmitterprepare/agree/load emission, byte-identical when no column overlaps), D5 (the@mutationINSERT structural dedup inTypeFetcherGenerator.insertColumnPlandriving the column list plus a single coalesced typedField<ColType>VALUES cell, turning the Postgres "column specified more than once" crash into one column + one agreement-checked cell), and the single-row UPDATE SET agreement preamble (emitSetAgreementPreamblebefore theMap.put`s). D2 moves the build-time-decidable half (two-or-more plain `@field`s on one column, a pure schema fact no runtime input could reconcile) to a validate-time reject on both paths: `MutationInputResolver.rejectPlainColumnCollision(INSERT) andUpdateRowsWalkervia the newUpdateRowsError.PlainColumnCollision(UPDATE, single-row + bulk), the mutation mirror of the R336@servicereject. All checks are presence-guarded (an omitted nullable writer is not a writer and cannot conflict) and pairwise against the first present writer (equalstransitive). Scope call: extended past the spec’s INSERT focus to the single-row UPDATE SET path, and carved the bulk UPDATE SET decode-overlap to R342 (Backlog) on the grounds that the bulkUPDATE … FROM (VALUES …)join fails loud (a duplicate derived column) rather than silently, so it is self-announcing; with the single-row SET agreement and the all-plain reject in place, no silent drop remains on any mutation write path. Coverage: execution-tierNodeIdValueAgreementExecutionTest(the agree / disagree / presence-guard matrix across all three paths plus the format-variant"01"case pinning the coerced comparison, onstorage_bin/film_endorsementfixtures), pipeline-tierJooqRecordServiceParamPipelineTest(two identity decodes / plain-field-plus-decode admitted and deferred to runtime) andMutationDmlNodeIdClassificationTest(the two-plain-fields rejects on INSERT and UPDATE SET), and theRejectionSeverityCoverageTestaudit for the new sealed variant; no generated-body string assertions. Documented residual (recorded in the R342 follow-up): the column→writers overlap analysis now exists in per-path instantiations (@service, INSERT, single-row UPDATE SET, the two validate-time rejects); the shared predicate has one home but the structural grouping is re-derived per carrier model / pipeline stage, the lift to one carrier-agnostic writer abstraction deferred to R342 when the bulk-SET path forces it. Spec → Ready and In Review → Done both reviewed by sessions distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R344 (
21915b9exception + loader + DevMojo arm + tests;1bf9ca7self-review tightenings + R345 follow-on;63b033dIn Review): Surface schema parse failures as clean dev-watch diagnostics, not infrastructure stack traces. In thegraphitron:devwatch loop a syntactically invalid schema (the common mid-edit case) dumped a ~30-frame graphql-java + executor stack trace into the build log on every keystroke, becauseRewriteSchemaLoader.loadwrapped graphql-java’sInvalidSyntaxExceptionin a bareRuntimeExceptionandDevMojo.runGeneratorPasslogged that through the "infrastructure" armgetLog().error(…, e)(with the throwable). A new typedSchemaParseException(a sibling ofValidationFailedException, not a subtype, inno.sikt.graphitron.rewrite) is now thrown from the loader’sInvalidSyntaxExceptionarm only; theIOException/ missing-file arms stay bareRuntimeException(genuine infrastructure, keep their trace). ItsgetMessage()is the existing file-attributed one-liner ("Schema parse failed in<file>at line N column M:<brief>`"), so the three already-quiet catalog-refresh paths (`regenerate/rebuildCatalog/buildOutputQuietly, which catchRuntimeExceptionand printgetMessage()) keep printing attribution unchanged; it also carries a nullableSourceLocation+briefconsumer-less for now, positioned to feed the deferred LSP-squiggle follow-on (R345).runGeneratorPassgains acatch (SchemaParseException)arm ordered before the generic infrastructure arm that logs the one-liner without the throwable and resetspreviousErrorKeys = null(a parse failure is not a validator verdict, so it must not feedWatchErrorFormatter’s delta tracker). `GraphQLRewriteGeneratoris unchanged: the exception propagates as-is throughloadAttributedRegistry()out of all three entry points with no translation step, so the one-shotvalidate/generatebuild still fails on a broken schema carrying the attributed message. Two reviewed deliberate deviations:runGeneratorPassrelaxed private → package-private as a test seam, and the consumer-lessbrieffield carried per spec for R345. Rejected fork (documented in the retired spec): routing throughValidationFailedException+ a fabricatedValidationError/Rejection.InvalidSchema, which would regress the quiet paths to a count string, falsifyValidationFailedException’s javadoc invariant, and stamp a pre-classification failure as a validator verdict. Coverage: unit `RewriteSchemaLoaderTest(throwsSchemaParseExceptionwith the offending file’sSourceLocation+ attributedgetMessage()pinned exactly to location + brief as the quiet-path regression guard; still bareRuntimeExceptionfor a missing file),SchemaParseExceptionPropagationTest(generate()propagates the same type, no translation), andDevMojoTest(malformed schema → parse arm logs no throwable; missing file → infrastructure arm logs the throwable, pinning catch-arm ordering); no generated-body string assertions. Spec → Ready and In Review → Done both reviewed by sessions distinct from the implementer. Out of scope: the LSP red squiggle for parse failures (deferred to R345), wire/attribution mechanics, any change toValidationFailedException. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R300 (
8249abbfixture + jOOQ-regen foundation;0074bd4model +@routinedirective +JooqCatalog.resolveTableValuedFunction+RoutineDirectiveResolver+ emitter;076cc56execution-tier proof +routine.adocdocs + fixture gates;b0533b1spec markup): First-class jOOQ routine support, day-one table-valued read slice. A new@routine(name:, argMapping:)directive backs a rootQueryfield with a jOOQ-generated table-valued function (PostgreSQLRETURNS TABLE/SETOF). jOOQ models such a function as a first-class catalogTable<R>, so the slice rides the existing@table-bound return-type and selection-narrowing ($fields) machinery unchanged; only theFROMsource differs, becoming a call to the schema’s globalRoutinesconvenience method with the routine’s IN parameters bound from GraphQL arguments. The provenance is carried by a newRoutineRef(the catalog-handle twin of@tableMethod’s `MethodRef) on a newQueryField.QueryRoutineTableFieldleaf, added toTypeFetcherGenerator.IMPLEMENTED_LEAVESso the four-way dispatch partition stays exhaustive;operation()isFetchandtarget()projects a bareTargetShape.Table, following theQueryTableMethodTableFieldprecedent. The deferred scalar-read and procedure-write forks reject at validate time viaJooqCatalog.resolveTableValuedFunction(they do not resolve as table-valued functions), satisfying "validator mirrors classifier" without aSTUBBED_VARIANTSentry, since no leaf is minted for them. Three reviewed deviations from the spec-as-reviewed: emission rides theRoutinesconvenience method rather than<ROUTINE>.call(…)(same SQL); catalog-based discovery with IN-param names depending on-parameterscompilation; classification reusesFieldClassification.QueryTableMethod(a dedicatedQueryRoutineis a follow-up). Coverage:ClassifiedCorpusroutine-table-valued-readfixture (Query / Fetch / List(Table)), a@ProjectionFor(QueryRoutineTableField)test inGraphitronSchemaBuilderTest, andRoutineFieldExecutionTestrunning the driving function (tilganger_for_feidebruker_med_fs_fiktivt_fnr) end-to-end against PostgreSQL including a selection-narrowing case; no generated-body string assertions. Spec → Ready and In Review → Done both reviewed by sessions distinct from the implementer. Deferred follow-ups (named in the retired spec): the procedure-writeOperationwrite arm, scalar-function and record-returning reads, child-positioned@routine, heterogeneous binding sources, and the translation of legacy’s 26procedureCall*rejection fixtures (the follow-up should also add pipeline coverage for day-one’s own resolver rejection arms, not only the legacy forks). Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db). -
R336 (
688a43cD1-D4 code + tests +CustomerRecordServicefixture; doc-fix rework13695d0): Flatten nested input-object fields in jOOQ-record@serviceparams. A@serviceparam typed as a generated jOOQTableRecordcan now group its columns under nested directiveless input objects that flatten onto the one backing table on the column axis, the analogue of the@table-input nesting the filter axis already supports. D1:CallSiteExtraction.ColumnBinding/RecordKeyDecodecarry an ordered, non-emptyList<String>access path (was a singlesdlFieldName) with aleaf()accessor; the last element is theMapkey, earlier elements the enclosing nested-input field names, and a top-level binding is a single-element path byte-identical to before (adopting theNestedInputFieldrepresentation R186 settled). D2:InputBeanResolver.buildJooqRecordrecurses into nested grouping inputs via a newcollectJooqBindings, threading the existingClassifyContextSDL-type-nameexpandingset for cycle detection, parallel to the member-axisbuildInputBeanwalk rather than routing throughclassifyInputField(a different carrier family on a different axis). D3: typedRejection`s through `JooqBuilt.Failfor cycle, list-valued nesting (a single record has one value per column), nested@table(a second DML target, cites R122), and plain-column collision across nesting (decode-vs-decode / decode-vs-column overlaps stay with R322’s value-agreement deferral). D4:JooqRecordInstantiationEmitterwraps each multi-element binding in a null-safe parent-Mapdescent (theinstanceof Map<?,?>chain idiom fromArgCallEmittergeneralised to statement form) with collision-freecamelJoin-derived locals; an absent / null / non-Mapgroup skips the columns under it, and a non-null identity inside an absent nullable group is skipped rather than thrown (skip-not-throw, since its R195 throw lives in the never-entered block). At depth 1 no wrapping block is emitted and the output is byte-identical to the pre-R336 form. graphql-java constraint surfaced in the execution tier: its coercion drops an explicit-nullfield from a nested input-object value (both literal and variable wire paths) while retaining it on the top-level argument map, so a present-nullnested leaf is indistinguishable from omitted and leaves the column untouched; the top-level present-null→NULLthree-way narrows to a nested two-way, with no emitted-code change. Coverage: pipelineJooqRecordServiceParamPipelineTest(8 new cases: flatten with two-element paths, mixed top-level + nested, nested@nodeIddecode, depth-2, and the four D3 rejections by message substring) and 6GraphQLQueryTestexecution cases (lands-on-column + omitted sibling, present-nullcollapse asserted through the variable path, null group, skip-not-throw on an omitted nullable identity group, empty-input, malformed-id-in-present throws), plus the compile tier type-checkingcreateCustomerRecordagainst the real jOOQCustomerRecord; no generated-body string assertions. NewCustomerRecordServicefixture (CustomerRecordavoids thecreateFilmRecordper-record-class dedup clash onQueryFetchers). Spec → Ready and In Review → Done both reviewed by sessions distinct from the implementer. Out of scope: multi-table nesting, reclassifying the nested grouping type (thePojoInput(null)model/LSP-honesty wart is R337), any change to the@table-input nesting path. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R340 (
006999c): LSP goto-definition for intra-schema type references. The cursor on a GraphQL type reference (a field/arg/input-field type, animplementsinterface, or a union member) now jumps to that type’s canonicaltype Foo { … }declaration in whichever open workspace file declares it; previously goto-definition resolved only directive arguments (@table/@field/@reference) into the jOOQ-generated Java tree. AddsIntraSchemaDefinitions(a provider in thedefinitionpackage parallel tohover/DeclarationHoversbesideHovers): it keys on the cursor sitting on anamed_typereference name outside any directive, skipsTypeNames.BUILTIN_SCALARS, walks open files via the per-URI lock-guardedWorkspace.getfast-skipping on the immutabledeclaredTypes(), and resolves through the newDeclarationKind.findDefinitionhelper (returns thenamenode of the canonical non-extension declaration, so navigation lands ontype Foo, notextend type Foo); the returnedLocationcarries the real declaration-name byte range viaPositions.toLspPosition, not the jOOQ path’s0:0placeholder. Wired afterDefinitions.computewith.or()inGraphitronTextDocumentService.definition(); the two paths key off disjoint syntax (anamed_typenever sits inside a directive argument) so they never contend. Coverage:IntraSchemaDefinitionTest(9 cases through a realWorkspace: same-file, cross-file,implementsinterface, union member, input field, built-in scalar empty, unknown type empty, cursor-on-declaration empty, definition-wins-over-extension; assertions on the returnedLocationURI +Range, not walk internals). Out of scope: extend-block navigation, find-references, the JavaParser-gated jOOQ per-line refinement (R90). Filed and implemented as R335; renumbered to R340 on landing after a parallel session allocated R335 to a different item (the input-surface classify-and-emit walk fold) that reached trunk first. Spec → Ready reviewed and In Review → Done approved by a session distinct from the implementer. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules incl.graphitron-lsp). -
R316 (slices 1-3b landed pre-rebase in the squashed root; slice 4a
9f643a7+ 4b09f3868; slice 5e522fb4+ prose sweepc900455+code-generation-triggers.adoc3e3f607+ changelog forward-notes903e84d; In Review transition70ce94e): Pivot the field-dimensional model fromcarrier × intent × mappingto(source, operation, target). A field is an edge: it arrives into asource(a wrapper around aSourceShape, the arm being the arrival cardinality and the emit-strategy dispatch:Root|OnlyChild|Child), performs anoperation(a sealed interface with payload-carryingrecordarms replacing the flatIntentenum:Fetch/Paginate/Lookup/ServiceCall/Count/Facet/Nest/NodeResolve/EntityResolve/ the writes), and projects atarget(aSingle|Listwrapper around aTargetShape, output cardinality read off the GraphQL return type).SourceShape ⊆ TargetShape; cardinality lives only as a wrapper bound to an endpoint, never a free enum (the wrapper-algebra invariant). Built populated, not abstract:ServiceCallcollapses the formerQueryService/MutationServiceverb pair (read/write is theSource.Root.Query/Source.Root.Mutationlegality gate now), holding the two un-unified call carriers in a transitionalCallholder pinned to R314; the fusedMapping.TableConnectiondecomposes intoTarget.Single(Connection)(shape) +Operation.Paginate(windowed-read verb); declared-gap arms (Count/Facet/EntityResolve/UpdateMatching/DeleteMatching, plus theInterface/Unionparticipant payloads) are modeled-but-unpopulated with documented known-gap entries. Slices: 1 rewrote the R222 model (docs); 2-3 introducedsource()/operation()/target()additively (bridges deriving the retired axes so the corpus classified unchanged); 4a re-derivedOutputField.requiresReFetch()+ thedispatchPerformsReFetchvalidator mirror over the new axes (the bareTargetShape.Table× holds-records guard, behaviour-preserving againstmapping() != Mapping.Table); 4b migrated the@classifiedR281 corpus ontosource:/operation:/target:+sourceShape:/targetShape:, recutDimensionTupleto compare at the altitude the directive can express (Sourceby structural equality,Operationby arm type token,Targetby(wrapper, outer-shape)token pair), and deleted the bridges and the four retired types (Carrier/Intent/Mapping/SourceCardinality); 5 landed the thoroughness gate. Tests:WrapperAlgebraTest(thesourceWrapperIsTheFoldOfAncestorTargetWrappersinvariant, target half mirrored against the parsed SDL output wrapper, source half pinned at the conservativeChildstrength R305 builds, with the connection decomposition and scalar-projection-leaf exemptions guarded against rot);RetiredDimensionTypesAreGoneTest(type-resurrection backstop: deleted files, no model-package imports, distinctive namesIntent/SourceCardinalityabsent as whole words, carve-outsSourceShape/LookupMapping/MappingEntryretained);ClassifiedDslTest.everyDimensionValueIsExercised(the disjoint-exhaustive coverage partition over thesource/Operationseals with the SDL-vs-Java name mirror);ReFetchDerivationTestmigrated behaviourally onto the new axes;SourceShapeProjectionTestretained as the source-arm projection guard. No code-string assertions on generated method bodies. The recommendedleafReconstructsFromCoordinatecompleteness pin is deferred (the old flat-enum corpus never pinned payloads either, so nothing regressed). ThedispatchPerformsReFetchmirror survives R316 by design (retiring it is R314’s emit re-platforming). Downstream: theSourceKeydecomposition becomes the first concrete consumer once this pivot lands. The R290 / R299 / R305 changelog entries carry forward-notes flagging their dimensional vocabulary as historical. In Review → Done review (claude/r316-review-mj53fc) swept three residual retired-vocabulary remnants slice 5 missed (two dangling{@link #intent()}/{@link #mapping()}method-links to deleted methods inOutputField/ChildField, andReFetchDerivationTest’s class-prose) onto the new vocabulary. Full reactor green (`mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules incl.graphitron-lsp). -
R315 (
0bb7161+ nullable-same-table-identity execution rework0d4acca): Bind FK-reference@nodeIdonto jOOQ-record@serviceparams. Generalizes R311’s same-table identity case to cross-table foreign-key references: a@serviceparameter typed as a generated jOOQTableRecordcan now be populated from an input whose@nodeIdfields reference other node types (the status / history / junction-row shape), with each decoded key mapped through the catalog FK constraint to the FK’s child columns on the record. Ports legacyNodeIdReferenceHelpers.mapKeyColumnsThroughForeignKeyinto the rewrite’s model. Model (D1):CallSiteExtraction.JooqRecord.keyDecode(Optional) becomeskeyDecodes(List, so a record may carry several@nodeIdfields);RecordKeyDecodegeneralizes R311’skeyColumnstotargetColumns(the resolved columns on this record the decoded values load into, identity or FK-child) and gains anonNullflag. Deliberately noKeyProjectionsub-axis: both arms loadtargetColumnsidentically, the identity-vs-FK distinction lives only in the resolver, and nothing downstream branches on it. FK resolution (D3): the FK-orientation-and-pairing core extracts out ofBuildContext.synthesizeFkJoinintoresolveFkSlots, so the join path and the newresolveRecordFkTargetColumnsshare one bug-fixed orientation site (parent columns fromForeignKey.getKeyFields(), notgetKey().getFields(), which mis-pairs a composite FK whose referenced-column order differs from the parent PK order); target columns reconcile to node-key (decode) order by column identity, not positional zip (a reordered FK whose referenced order differs from the node key would otherwise mis-assign every value). FK deduced when exactly one connects the two tables, else named verbatim by@reference(path: [{key:}])(only the first path element is consulted for record population). Convergence by rejection (D2):@tableon the input classifies it asTableInputType("Graphitron owns the DML"), which contradicts a jOOQ-record@serviceparam ("the service owns the DML");InputBeanResolverkeeps R311’sJooqTableRecordInputTypetrigger and adds a narrowerisTableRecordreject arm so a@table-present record param fails honestly ("drop@table…") instead of falling to the bean path’s misleading "has no fields matching." Null semantics (D4):JooqRecordInstantiationEmitterswitches from a singlefromArraybatch to per-binding conditional loads keyed onraw.containsKey(…), applied uniformly to@fieldcolumns and each@nodeIddecode: a non-null (ID!/!) binding always loads and throws on a null / wrong-type decode (R195); a nullable (ID) binding leaves an omitted column unwritten (changed=false, excluded from the service’s INSERT/UPDATE), sets a present-nulltoNULLviaset(field, null)(reliable changed flag;fromArraynull-skips), and decodes-and-loads a present value; coercion stays on the non-deprecatedfromArraypath. Two R311 behavior changes carried, folded in deliberately (D4): (1) R311’s same-table identity singlefromArraybatch becomes per-binding conditional loads; (2) a nullable (ID) same-table identity moves from always-throw-on-null to skip-when-omitted (a service-side upsert input: omitted → unset PK → the service-owned INSERT lets the DB assign it). The emitter’s "two disjointfromArraygroups" javadoc and theRecordKeyDecode"always throws … whetherID!orID`" javadoc are both retired so neither becomes a false invariant. Rejections (build-time `UnclassifiedField): zero/multiple FK without@reference(key:); a node key column not covered by the chosen FK; an explicit@referenceon a same-table@nodeId(a self-FK request, out of scope, preserving legacy’s loud forbiddance instead of silently writing the record’s own PK); (unchanged R311)@field→no column, cardinality parity,@nodeIdwithouttypeName. The R311 single-@nodeIdgate is removed: multiple@nodeIdis now legal (each resolves independently; overlapping-load-column value-agreement is a runtime concern deferred to R322, last-write-wins here, never hit by the motivating consumer whose references are disjoint-column). Coverage: pipelineJooqRecordServiceParamPipelineTest(21 cases: FK deduction, the renamed-FK target columnendorsed_film≠film_idreal pin, reordered composite-key decode-order reconciliation, explicit@reference(key:)disambiguation on the two-FKstudierett, the@tablereject, mixed identity + FK + plain@field, the full rejection set, the formertwoNodeIdFields_rejectflipped to a positive two-keyDecodesclassification) +SynthesizeFkJoinReorderedKeysTest(slot orientation pinned for both consumers); execution (sakila) the FK-child INSERT readback proving the decoded id lands on the renamedendorsed_filmchild column, the nullable FK-reference / nullable plain-column omitted-vs-null-vs-setchanged-flag contract, and (rework) the nullable same-table identity onAddressRecord(omitted → DB-assigned serial PK via the service-owned INSERT, set → decoded id on the PK), the only tier that observeschanged=falseexclusion; compile (graphitron-sakila-example)createFilmEndorsementRecordagainst real jOOQ at Java 17. Adds thepublic.film_endorsementrenamed-FK fixture (FK childendorsed_film≠ referencedfilm_id) and reusesidreffixture.studierett/nodeidfixture.reordered_fk_child/child_ref. User docs: FK-reference@nodeIdon@servicejOOQ-record params documented innodeId.adoc. Generalizes R311; the@table-on-input deprecation /argMappinggrouping stays R97, self-reference and pojo member-axis FK-@nodeIdare separate items, overlap value-agreement is R322. Motivated by fourutdanningsregisteret-graphql-specstatus-mutation consumer shapes. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R325 (folded into R317, no implementation shipped): Classify in a single field-first visitor walk (retire the eager type pass). Filed 2026-06-17 as a from-first-principles restatement of R317’s goal, then folded into R317 the same day and discarded as a separate item. Its substance lives in R317’s "Read-free visitor invariant and the single walk" section: the read-free visitor invariant (the classifying visit may only
register, never read the registry under construction), parent context down theSchemaTraverservar channel, reconciliation in the registry (the visit stays pure), the parent-independent-entry guard that dissolves the dedup worry, the explicit non-field edges (@node/@keyseeds, interface<→object structural edges, per-usage input resolution), validations as named post-walk passes, the anti-narrative slicing rule (no remaining slice may be structure-only), and the falsifiable acceptance test (no type registered before its discovering field is visited). The one mechanism fork it raised, no global pre-pass fixed points, was settled by treating a precomputed fixed point passed as an explicit traverser argument as read-free-compatible: theNodeIndexstays precomputed and threaded in, reflection grounding becomes on-demand (noRecordBindingResolver.resolveAll()precondition). R317 stays backwards-compatible: it restructures type classification and keeps the leafChildFieldmodel untouched; the genuinely distinct idea this thread surfaced, classifying fields directly into the dimensional(source, operation, target)model so the field carries its own type/table binding, is a separate switch off the leaf model that lives with R316 (the(source, operation, target)pivot) and R314 (dimensional emit, where the leaves dissolve), not with the R317 driver. (Independently discarded from trunk’s side as1ac9c75; the two discards reconciled here.) Number retired, not reused. -
R324 (
45bf3cb): Lift the single-cardinality multi-hop@splitQueryrestriction. A single-cardinality@splitQuerychild field (returnsT, not[T!]) whose@reference(path:)had more than one hop was rejected at classification time (FieldBuilder.classifyObjectReturnChildField’s `FieldWrapper.Single && elements().size() != 1Rejection.deferred), even though the supporting classification machinery (deriveSplitQuerySourcekeying offpath.get(0),BuildContext.buildParentCorrelationpinning onlyfirstHop) was already hop-count agnostic. The gap was entirely inSplitRowsMethodEmitter: of the three cardinality siblings onlybuildSingleMethodnever grew the bridging-hop loop, projecting/FROMing offfirstAliaswith a single-hop-only(firstAlias, firstAlias)per-hop WHERE shortcut. The fix extracts two shared private helpers retiring the list/single/connection topology duplication that caused the drift,emitFromBridgeAndParentJoin(FROM-terminal + bridging-hop loop +OnConditionJoinparent JOIN +parentInputcorrelation) andbuildWhereCondition(per-hop FKwhereFilter`s + field-level filters); all three siblings route through both, and the connection WHERE loop’s unconditional `(JoinStep.FkJoin)cast becomes the sharedinstanceof-guarded form.buildSingleMethodnow projects/FROMs offterminalAliasand bridges multi-hop, returningscatterSingleByIdxunchanged; single-hop paths collapse the bridging loop to a no-op, so theRecordTableFieldemitsSingleRecordPerKey()path that also routes throughbuildSingleMethodemits exactly as before (audited, no change required). Bridging hops are inner joins consistent with the siblings: a to-one chain resolves tonullwhen any hop is absent; distinguishing intermediate-null from terminal-null (LEFT JOINs) stays out of scope. The classifier guard is removed; the motivatingCustomer.storeAddressshape (customer → store → address) now classifies asSplitTableFieldwith a 2-hopjoinPath. Coverage: pipelineGraphitronSchemaBuilderTest.SPLIT_TABLE_MULTI_HOP_SINGLE_CARDINALITY(the former_REJECTEDenum, now a positive 2-hopFkJoinassertion keyed onstore_id), executionGraphQLQueryTest.splitTableField_singleCardinality_multiHop_bridgesToTerminalAddressPerCustomer+_dedupesSharedKey_oneBatchRoundTrip(correctAddressper customer matching the inlinestoreAddressnavigation, and a single batched rows-method round-trip); the null-where-no-match semantic is structurally covered byScatterSingleByIdxTest+ the existing single-hop null-FK fixtures since Sakila’scustomer.store_id/store.address_idare both NOT NULL. The now-false "single-cardinality multi-hop requires split" bullet is dropped fromsplit-vs-inline.adoc(the shape works inline and via split). Emitter-only; no model-shape, directive, or wire-format change. Motivated by asis-graphql-specconsumer shape. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db). -
R23 (
c38779e): Multi-parentNestingFieldsharing,TableFieldarm. Lifted the multi-parent shared-shape gate inGraphitronSchemaValidator.compareNestedFieldsShapeforChildField.TableField: a plain-objectNestingFieldtype may now be shared across multiple@tableparents when its shared leaves include inlineTableField`s, where before the catch-all rejected them with "not yet supported across multiple parents". The arm admits the pair without further shape comparison, the upstream class-equality gate (`continueon agetClass()mismatch) already guarantees both sides areTableField, andreturnType()derives from the single SDL declaration on the shared nested type so it is identical by construction; per-parentjoinPath/filters/orderBy/paginationare intentionally not compared because each parent’s$fieldsemits its own correlatedDSL.multisetarm. No emitter or wiring change:TableFieldis aPROJECTED_LEAFwhose reified read (FetcherEmitter.bind, wrapped inLightFetcher) pulls by field name from the sourceRecordwithout consulting the outer parent table, so first-parent-wins nested-type registration has no runtime effect for this leaf. Coverage: pipelineGraphitronSchemaBuilderTest#multiParentSharedNesting_inlineTableFieldLeaf_classifiesAndValidatesPerParent(two@tableparents sharing a nested type whoseaddressleaf classifies as aTableFieldwith a distinct per-parent FKjoinPath; validator emits no error), executionGraphQLQueryTest#multiParentSharedNesting_inlineTableField_returnsAddressPerParent(sharedOccupantLocationacross Customer and Store, each resolvingaddressper parent, the Customer side pinned to the same row as the directCustomer.addressFK navigation), and the existingNestingFieldValidationTestnon-TableFieldrejection stays green. Retired the stale#8roadmap pointer in the catch-all comment; filed the BatchKey-leaves follow-up (and the openLookupTableFieldre-scoping question) as R323. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R321 (
6601e39): One-shot mojos (graphitron:validate/generate) now renderValidationFailedException.errors()in the Maven failure output, at parity withDevMojoand theSchemaProblembranch.AbstractRewriteMojo.runGeneratorpreviously special-casedSchemaProblembut letValidationFailedExceptionfall through to the genericcatch (RuntimeException e)arm, which rethrew only the exception’s message (the bareN schema validation error(s)count) so a consumer build (opptak-subgraph) saw nofile:line:coldetail. The fix adds a siblingcatch (ValidationFailedException e)arm ahead of the generic one, mirroring theSchemaProblemarm: it wraps the cause in a null-message intermediary (so Maven’sDefaultExceptionHandlerdoes not append the bare count after the detail) and keeps the exception on the cause chain for-e/-X. Rendering is factored into a package-privatevalidationFailureMessage(List<ValidationError>)that prepends a"GraphQL schema validation failed:"header (matching theSchemaProblemDiagnosticarm) toWatchErrorFormatter.format(errors, null), the same renderer thegraphitron:devloop uses (nullprevious-key set drops the dev-only delta line), so the one-shot and dev surfaces share one renderer and cannot drift. This covers errors raised at any build stage (validate(), theGraphitronSchemaBuilder.buildBundlefederation-recipe rewrap, orTagLinkSynthesiser.apply). Coverage:AbstractRewriteMojoTestasserts the message carries per-errorfile:line:coldetail (not just the count) and embeds the exact treeWatchErrorFormatter.format(errors, null)produces (structural dev-loop parity), at the same formatter-level tier as the siblingSchemaProblemDiagnosticTest. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db). -
R318 (subsumed into R317, no implementation shipped): Validation registers diagnostics without reclassifying (immutable validate phase). Filed 2026-06-17 as a follow-on to R317’s single classification pass, then inlined into R317 the same day as its closing slice when R317 was rescoped from a byte-identical reorder to the full single-edge-driven-classify-pass + immutable-validate arc. No standalone work shipped; the immutable validate phase lands under R317. Number retired, not reused.
-
R279 (slice 3b inversion
ee77a33, slice 4507242f, slice 54a1a117, slice 6 prune0e13c12+ verb-collapse2437a84; In Review transitiondf6c1f7; slices 1/2/3a landed pre-rebase): Field-first reachability-driven classification driver. Replaces the eager type-pass / all-objects field-pass / four-post-pass sequence with a reachability-driven, field-first walk that classifies each type as a byproduct of the field edge that reaches it, then validates, then emits. R222 slice; supersedes R166 (the reachability prune is structural here, not a per-emitter skip-filter), and spins R166’s typed-non-empty-carrier sub-thread to R280. The walk seeds Query + Mutation + Subscription roots plus a@node/@keydirective scan, and descends output edges (field→target, union→members native; interface→implementor and object/interface→interface via the customSchemaTraverserchild function, the load-bearing fan-out that keeps a directly-seeded federation implementor from pruning theNodeinterface itsimplementsclause references). The accumulator owns reconciliation:TypeRegistry.registeris the sole write verb (the Q2 verb-collapse), reconciling repeated registrations three ways (equal → idempotent, compatible → merge incl. the cross-carrier federation@tagunion andshareableOR for synthesised Connection/Edge/PageInfo, incompatible → demote toUnclassifiedType); the formerclassify/enrich/synthesize/demoteverbs dissolve into it, the traceOpderived from the reconciliation arm so per-call observability survives. The classifier is a pure producer reading only node SDL + reflection + downwardTraverserContextcontext (never sideways/back), making verdicts order-independent;TypeBuilder.findReturnTablesForInput’s global back-scan dissolves into a local field-visit read. What shipped across the slices: order-independent `participantClassification(3a, replacing the sidewaysctx.types.getread); the field-first driver inversion with compensating orphan sweeps (3b);DomainReturnTypeenforcement relocated from a reclassifying post-pass to aGraphitronSchemaValidatorrule (collectDomainReturnTypeConflicts+validateUniformDomainReturnType, model change + validator rule in one commit, no enforcement gap);ConnectionPromoterfolded into the walk assynthesiseForFieldwith a single-producerrebuildAssembledForConnections(5); orphan prune made observable (an unreachable@tableobject is no longer classified) and the verb-collapse completed (6). The down-the-walk context admits ancestor-cardinality accumulation as a first-class rider so R308 can compute the source-cardinality ancestor-product without re-walking; R279 itself stays behaviour-preserving and does not compute it. Honest residual: the "true single-pass DFS fold" (approach A, inlining type classification into the field visit and deletingTypeBuilder.buildTypes) is deferred to R317 (now Ready); the driver is field-first and the walk is the sole classifier, butbuildTypesstill hosts the type-classify loop over the walk’s reachable set. Coverage:SchemaReachabilityTest(thereachable ⊆ classifiedsafety invariant, hardened at slice 6 to "every classified output composite is reachable"),ConnectionAssembledDeltaPipelineTest(the assembled-schema delta, the projection differential’s blind spot),ProjectionSnapshotComparator(dev bisect aid only, never the gate), and the designated primary gate throughout: theGraphitronSchemaBuilderTestexhaustive truth table + sakila pipelineTypeSpec+ the Java-17graphitron-sakila-examplecompile + the PostgreSQL execution tier; assertions are on classified-model/assembled-schema structure, no generated-body code-string matches.FieldBuilder’s ~5660 lines of per-field logic untouched (the change is the driver, reachability, and registration); LSP `TypeClassification/FieldClassificationprojections confirmed unaffected. Forward edge R279 → R308; supersedes R166. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R311 (
c0e8626): Bind a jOOQTableRecord(singularRecordorList<…>) directly as a@serviceinput param. A top-level@serviceparameter whose Java type is a generated jOOQTableRecordalready classified asJooqTableRecordInputTypeon the type side but could not be bound at the call site:InputBeanResolver.enrichbean-ified it on the Java-member axis, so a@fieldnaming a column matched nothing and the build rejected with a misleading "has no fields matching".enrichnow reads the already-classified type (table and all) just after the shared input-object gates (loadable /Map/ cardinality-parity) and binds on the column axis instead: each plain@fieldfield resolves to aColumnRef(aCallSiteExtraction.ColumnBinding), and a single@nodeIdfield decodes the record’s scalar key (R195’s wire mechanism projected onto the param record’s own identity, aRecordKeyDecode); a lifted record-type-mismatch gate rejects a foreign-table@nodeId. New model: theCallSiteExtraction.JooqRecordpermit + the column-axisColumnBinding/RecordKeyDecoderecords (siblings to the member-axisInputBean/FieldBinding, each carrying its ownsdlFieldNameMap key since neither rides aFieldBinding), with a compact-constructor at-least-one-binding floor; andValueShape.JooqRecordInput, a path-carrying leaf that also carries its construction carrier (theScalar-carries-leafTransformprecedent) so the helper-queue collector registers from theValueShapealone. A newJooqRecordInstantiationEmitteremits a dedupedcreate<Record>(Map)/create<Record>List(Object)helper pair (recordfromArray(…, Tables.<T>.<col>)for the columns, no deprecatedDataType.convert, +NodeIdEncoder.decodeValuesfor the identity), reached identically from the root emitter (ServiceMethodCallEmittervia the new leaf) and the child-coordinate emitter (ArgCallEmitterreal arm) ; the binding is coordinate-agnostic (enrichruns for child@servicetoo, before theisRootgate) and both cardinalities share one construction site (the plural maps the singular per element). TheTypeFetcherGeneratordual walk feeds one record-class-keyed dedup queue so either coordinate emits the helper exactly once. The two sealed additions force-flag every exhaustive switch: real arms where reachable (valueShapeExpression/listExpression/ArgCallEmitter.buildArgExtraction/collectFromValueShape), defensive/throw arms where aJooqRecord(Input)is never anInputBeanfield leaf. The misleading bean message becomes honest, validate-timeUnclassifiedFieldrejections (foreign-table@nodeId, two@nodeId, unresolvable column with a Levenshtein candidate hint, cardinality mismatch at the shared parity gate). Coverage:JooqRecordServiceParamPipelineTest(11 cases: singular, composite key, list →ListOf(JooqRecordInput), the regression pin for the original bug, the child coordinate, and the rejection set), fourGraphQLQueryTestexecution cases round-tripping the identity decode + column SET against PostgreSQL (singular, composite, list, wrong-type-throws), and the sakila-example compile tier type-checking the emitted helpers + the childArgCallEmittercall against the real catalog; no generated-body string assertions. A call-site param-binding NOTE lands on the@servicesurface incode-generation-triggers.adoc(the classification was already documented). Out of scope:Set<TableRecord>(inherits theInputBeanpath’s imperfectSethandling), FK-reference@nodeIdand@table-on-input (both R97). Motivated by theendreUtdanningsspesifikasjonsstatus(List<…Record>)consumer shape. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R310 (
da56aa8): Name the forbidden directive on an otherwise-valid DML payload carrier’s data field. A@mutation(typeName: INSERT/UPDATE/DELETE)field whose payload’s single data field carried a DML-forbidden directive (e.g.@splitQuery) was rejected with the misdirected generic"is not yet supported; use ID or a @table type", pointing at the (fine) return type rather than the one-token edit on the data field that actually disqualified it. A newBuildContext.diagnoseForbiddenCarrierDirectivewould-admit-but-for-the-directive probe re-runs the structural DML scan under a privateForbiddenDirectivePolicy.IGNOREgate, leaving the public scan contract (scanStructuralDmlPayload/scanStructuralServiceCarrierPayloadand every speculative caller) byte-for-byte unchanged; when the payload would admit as a carrier but for the forbidden directive, the singleScalarReturnTypearm ofMutationInputResolver.validateReturnType(where all three DML kinds converge) surfaces a targeted message naming the data field and the@-prefixed directive, with the@service-carrier@splitQueryasymmetry note (R275, thewarnIfSplitQueryOnRecordParentadvisory) appended conditionally. The generic message stays the fall-through for genuinely unsupported scalar returns. The rejection keeps the uniform proseRejection.structuralshape of its siblingvalidateReturnTypearms; a typedReturnTypeErrorsub-seal lift is deferred. Coverage: fourMutationDmlCasepipeline cases (UPDATE + INSERT@splitQuerythrough two distinct routing paths pinning the "one arm covers all DML kinds" invariant, a non-@splitQuery@condition, and a negative-control would-not-admit two-data-channel payload confirming the probe does not over-fire), asserting load-bearing tokens onUnclassifiedField.reason()with no generated-body string assertions. Discovered during theutdanningsregisteretGraphitron 10 migration; sibling of R213 (message vs location). Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R305 (slice 1
b3f0f68, slice 2355f9b5, slice 3 collapse7e672b9+ re-fetch derivationd829d42, source-shape mirror18d3aee; In Review transitionsc6847c6+18d3aee): Forward note (R316, 2026-06-19): thecarrier/intent/mapping/ source-cardinality model this entry describes was pivoted to(source, operation, target); theCarrier/Intent/Mapping/SourceCardinalitytypes named below no longer exist. Read the dimensional vocabulary here as historical. Expand the carrier dimension with source-shape and cardinality; separate re-fetch from intent and collapseSingleRecordTableFieldintoRecordTableField. R222 Stage 3 follow-on to R290. The carrier dimension gains aSource-arm source-shape (Table | Record, the input-side mirror ofmapping’s `Table:Column :: Record:Field) and source cardinality (One | Many);Carrieris now sealed (Query/Mutationpayload-less,Source(SourceShape, SourceCardinality)). The deeper correction (settled 2026-06-15): intent and re-fetch are orthogonal. Intent classifies the target and how arguments are interpreted (Fetch,Lookup, the writes,QueryService); re-fetch is the derivedRecord-to-Tablecrossing.OutputField.requiresReFetch()derivesTable mapping × holds-records(Source{Record}received or a Service/DML intent produced), not from intent alone, catching the whole family (former-SRTF, RTF, RLTF, RTMF, STF);GraphitronSchemaValidator.dispatchPerformsReFetchmirrors it.SingleRecordTableFieldis deleted: the twoFieldBuildercarrier sites (R178 DML, R275@service) now buildRecordTableFieldviabuildPayloadCarrierRecordTableField(a singleJoinStep.LiftedHopover the target PK folding source=target onto oneColumnRef, plus a newSourceKey.Reader.ProducedRecordRead); intent staysFetch. The runtime-call insight that settled it: SRTF and RTF are the same shape, the carrier field is called once with the producer’s full held output and feeds the same Split-rowsVALUES(idx,pk) JOIN … ORDER BY idxscatter; only the key reader differs. Source cardinality is conservatively hard-codedMany(the absorbing element, always-correct as a one-element batch); the inlineOne-skip optimisation is kept as dead code until R279 computes the true ancestor-product cardinality.OrderingOwnedByProduceris deleted, itsvalidateListRequiresOrderingexemption replaced by a plainrequiresReFetchexemption (also fixing a latent bug: a PK-less idx-ordered re-fetch is now admitted rather than wrongly rejected). The 352-lineFetcherEmitterSRTF path is removed with the LocalContext null-source guard preserved; the LSPFieldClassification.SingleRecordTableprojection collapsed intoRecordTableTarget. One honestly-documented divergence: dispatch routes by leaf identity (everyRecordTableFieldbatches) rather than literally reading theManyslot at the fork; net effect matches the spec, R314 filed for the follow-up. Coverage:ReFetchDerivationTest(trueacross the Record-source family + mirror agreement),SourceShapeProjectionTest(walks every corpus-demonstratedChildFieldand cross-checkssourceShape()against the parent type’s independently-classified backing ;TableBackedType→Table, elseRecord; exercising both arms, with a reflective sealed-leaf guard overChildFieldwhose uncovered leaves must carry a documentedNOT_CORPUS_COVEREDentry; the validator-mirrors-classifier analogue ofdispatchPerformsReFetch), the@classifiedcorpus grown withsourceShape/sourceCardinalityargs and the former-SRTF row retargeted toRecordTableField/Source{Record, Many}/ intentFetch,SingleRecordPayloadPipelineTestinstanceofretargets,GeneratorCoverageTest.everyGraphitronFieldLeafHasAKnownDispatchStatusexhaustive/disjoint after the leaf deletion, and the load-bearing execution tier (SingleRecordPayloadDmlTest/SingleRecordTableFieldServiceProducerExecutionTest) preserving R141 / R158 / R275 payload-carrier behaviour (single + bulk,DIRECT+OUTCOME_SUCCESS,fjernSakTagg) end-to-end against PostgreSQL through the batched Split-rows path; no method-body code-string assertions. This is a deliberate leaf change, not byte-invariance: the former-SRTF coordinate’s emitted SQL becomes the batched idx-orderedVALUES-join scatter and its re-fetch verdict flips totrue, while the runtime result (same rows, same source order) is preserved. Out of scope: the@servicecarrier arriving as a list (R308), target-cardinality-many on theOnepath, and the broaderSourceKey.Cardinalitywrapper().isList()disentangling (rides R222). Full reactor green (mvn install -Plocal-db, 11 modules incl.graphitron-lsp). -
R309 (
82f2ba3): Descriptions for query-as-view projections. A# …line comment authored above a selected coordinate in a corpus doc-example projection query now renders as that coordinate’s SDL description inQueryViewRendereroutput: above a field it describes the field, above… on Tor a top-levelfragment f on Tit describes typeT; multiple comment lines join into a block-string"""…"""description, and a comment-free projection renders unchanged (the existing sevenQueryViewRendererTestcases stay byte-equal).Touchedgains type/field description side tables,Walkrecords them through the singledescriptionOf(Node)source seam (where native executablegetDescription()reads onFragmentDefinition/VariableDefinitionfold in once graphql-java is bumped past the pinned 25.0), andprune/keptFields/stripInternalDirectivesstamp them onto the rebuiltDescribedNode`s via a shared `applyDescriptionhelper.Fieldprose stays comment-sourced becauseFieldis not aDescribedNodein any graphql-java version, making comments the durable carrier rather than a stopgap. This is test-and-docs tooling undersrc/test, not the production generator path: thecatalogcorpus example’squery()gains comments so its rendered block oncode-generation-triggers.adoccarries field descriptions, andClassifiedDocTestguards that block verbatim. Coverage: five newQueryViewRendererTestpipeline cases (field description, inline-fragment type, top-level-fragment type, multi-line block string, comment-free no-regression pin), all asserting on rendered SDL rather than generated method bodies. Out of scope: the graphql-java version bump and native-description source read (thedescriptionOfextension), descriptions on production generator output, and operation-level (query { … }) descriptions. Full reactor green. -
R200 (
e7be7f4): Honor@field(name:)inInputBeanResolverfor@serviceinput-bean/record member binding. The resolver bound consumer-bean/record members by raw SDL-field name, reading zero directives: a Java member name diverging from the SDL field name lost the binding (JavaBeans rejected with "no fields matching"; records silently emitted an under-arity canonical-constructor call).bindingKey(f)now reads@field(name:)per SDL field as the Java-member binding key (the houseargString(f, DIR_FIELD, ARG_NAME).orElse(f.getName())idiom, the input-side mirror of R191’s output accessor axis), andbuildInputBeanBodyis restructured: the record-vs-JavaBean target is computed once and dispatched viaswitch (target)intobindRecord/bindJavaBean(the mid-loopisRecord()re-tests collapse;recordOrder/sdlOrderdeleted); a single collision-checkedsdlByBindingKeyindex shared by both arms rejects two SDL fields resolving to one member (ambiguity) and a present-but-blank@field(name: ""); the record arm enforces a total bijection inbindRecord(direction A: every component must bind, was a silent under-arity drop; direction B: every SDL field must be consumed, was a silent data drop), retiring the dead "has no component named" branch by construction, while the JavaBean arm keeps partial-population tolerance and the empty-bindings rejection. Per-field leaf classification factors into a sharedbindFieldreturning a builder-internal sealedFieldResult(Ok/Fail); no newCallSiteExtractionleaf, soInputBeanInstantiationEmitter’s exhaustive switch is untouched, and `FieldBindingkeepssdlFieldName(the wire/Mapkey) separate fromjavaFieldName(the member) so emit stays selection-agnostic. Rejections ride the existingBuilt.Fail→Rejection.structural→UnclassifiedFieldpath. The@fielddocstring gains the Java-member axis for both the R191 output (FIELD_DEFINITION) and R200 input (INPUT_FIELD_DEFINITION) sites, paying off R191’s output-axis doc debt. The R97org.jooq.*looksLikeBeanCandidateseam (jOOQ-TableRecord-as-param) and the R195@nodeIdjOOQ-record-member leaf are untouched. Coverage: pipeline-tier positive cases (record + JavaBean renamed via@field, assertingFieldBinding.javaFieldName()is the directive value whilesdlFieldName()stays the SDL name), rejection cases (direction A/B, ambiguity, blank value, all onUnclassifiedField.reason()), a regression floor (a divergent-name JavaBean without@fieldstill rejects "has no fields matching"); execution-tiersubmitFilmReviewSummary_routesThroughFieldRenamedRecordBeanround-trips through a@field-renamed record bean (the round-tripped reviewId proves positional binding); new fixturesTestInputBeanRenamed/TestInputJavaBeanRenamed/TestInputSubsetRecord/FilmReviewSummary. Input-side counterpart of R191; R201 / R202 carry the remaining@field-symmetry items. Full reactor green. -
R307 (Part A
2741787+ Part B5fe85f5; In Review transitioncce19f3): Retire stale@recordreferences.@recordstays a declared, legal-but-ignored directive (directives.graphqlsdeclaration +readRecordClassNameintact, nothing reads it to drive binding), but every treatment that implied it was live is gone. Part A rewrote the five rejection messages that steered authors toward authoring@record(dropping the never-valid@record(class:)form) to name the reflected-backing path instead, and renamed@record-as-jargon for "record-backed type" to "record-backed" / the variant name across main and test source. Part B: (1) the standaloneTypeBuilder.emitDirectiveIgnoredWarningspost-classification re-walk is replaced by a per-typeemitDirectiveIgnoredWarningcalled from the single classification pass, so the deprecation warning is a classification output; the three message variants (shadowed-by-@table, redundant/matches, disagrees) and the multi-producer-rejection suppression are preserved unchanged. (2) The LSP no longer treats@recordas a liveExternalCodeReference-className binding: no className FQN completion, no "Unknown class" diagnostic, no live-binding hover, and (a warranted extension past the spec’s literal three surfaces) no legacyname:→className:alias nudge. Because theInputField("ExternalCodeReference", "className")coordinate is shared with@enum, each surface gates on the enclosing directive name (mirroringMETHOD_VALIDATING_DIRECTIVES); the completion site gained the directive via a newdirectiveNamefield threaded fromLspVocabulary.CursorLocationintoCompletionContext. No@deprecated/deprecatedCoordinateswiring; the editor’s "ignored" signal stays the generatorBuildWarningalready surfaced throughDiagnostics.validatorDiagnostics. (3) Every applied@recordwas purged from test-fixture SDL in both modules: generator binding-hint drops are classification-neutral (reflection binds via the@service/@mutationproducer or@table), warning coverage consolidates into the newRecordDirectiveIgnoredWarningTest(three variants + suppression + reachability +@error-ignored +@table+@recordno-conflict, all at the classifier with no generated-body string assertions), the LSP carve-out fixtures keep@recordto assert the absence of tooling, andValidatorDiagnosticsTestpins the@record-ignoredBuildWarningsurfacing as a usage-siteWarning.BuildOutputReportPipelineTest’s report-wiring warning swapped from a redundant `@recordto a redundant@splitQuery(the test needs only a model warning). The only applied@recordleft in either test tree isRecordDirectiveIgnoredWarningTestand the LSP carve-out fixtures. Out of scope (deferred): removing thedirectives.graphqlsdeclaration. Full reactor green (mvn -f graphitron-rewrite/pom.xml install -Plocal-db, 11 modules). -
R264 (
98bafe8; In Review transitionef7bd17): roadmap-toolstatusround-trip no longer strips quotes from front-matter titles. Thestatussubcommand rewrote the block through a snakeyamlload-then-hand-serialize round-trip with no value-quoting, so a quotedtitle:containing": "(the common "subtitle: detail" shape, e.g. R256) came back as a bare string and was re-emitted as invalid YAML; the very next parse, including the README regeneration the same subcommand runs, threwScannerExceptionand left the file unreadable. Replaced the lossy write path withpatchFrontMatter, which rewrites only the named keys (status,last-updated) in place and leaves every other line, the body, and the fences byte-for-byte untouched, also removing the latent risk of the round-trip reformatting lists and dates. Routed the siblingwriteChangelogNextIdthrough the same helper, retiring its identical hand-serialization loop. Coverage:RoadmapTitleQuoteRoundTripTestruns a fullstatussubcommand (including the regeneration that was the live crash site) over a colon-bearing quoted title and asserts byte-for-byte preservation plus a clean re-parse, with a directpatchFrontMatterunit test for present-key replacement and absent-key append. -
R290 (
5ebc52cslice 1 +1227f0cslice 2 +84a49f2slice 4; In Review transition38006ad; docs passes84de71e+6ceafb0): Forward note (R316, 2026-06-19): thecarrier × intent × mappingmodel this entry describes was pivoted to(source, operation, target); theCarrier/Intent/Mappingtypes and thecarrier()/intent()/mapping()accessors named below no longer exist. Read the dimensional vocabulary here as historical. Field-side dimensional slots, materialisecarrier × intent × mappingon the field and dissolve the fused cross-product’s leaf-identity reads. R222 Stage 3. Slice 1:carrier()/intent()/mapping()land as three narrow accessors onOutputField(the field root that survives Stage 6), computed at classification time, reproducing exactly what R281’s throwawayLeafTupleAdapterreconstructed; the dimension enumsCarrier/Intent/Mappingmove into themodelpackage, the adapter is deleted, and the classified-corpus harness builds its test-sideDimensionTupleby reading the three accessors off the field (ClassifiedHarnessline ~114). Three accessors not a neutral tuple, per "narrow component types" and "sub-taxonomies carry distinct information": each consumer reads exactly the axis it forks on (legality readscarrier, polarity reads theintentfamily, build-vs-consume readsmapping, re-fetch readsintent × mappingjointly). The triple is a total classification (every field has all three), so noNo<Family>absence arm. Slice 2:ConstructorFielddissolved as wrong-by-design, a@tableparent constructing a@record/@servicechild from its own row, reachable only via self-referential test coverage. The classifier’sResultTypearm inFieldBuilder.classifyChildFieldOnTableTypenow rejects with anUnclassifiedFieldwhose structural rejectionGraphitronSchemaValidatorsurfaces as a build-time error; the leaf, its dispatch (IMPLEMENTED_LEAVES/FetcherEmitter/TypeFetcherGenerator/CatalogBuilder), and itsLeafTupleAdapterarm are removed. Theconstructorcorpus example leaves the classified corpus and becomesConstructorFieldValidationTest’s rejection fixture; the `GraphitronSchemaBuilderTestverdict +@ProjectionForsibling are deleted and theSingleRecordPayloadPipelineTest/DummyFetcherFixturesconstructor-child fixtures removed. Live leaves 49 → 48. Slice 4: the re-fetch derivation made real, the proof the slots earn their keep.OutputField.requiresReFetch()is the single home of the service/DML →@tablere-query predicate, derived fromintent × mapping(mapping == TableAND intent in{QueryService, MutationService, Insert, Update, Upsert, Delete}) rather than re-decided per leaf in the consumer;GraphitronSchemaValidator.validateFieldmirrors it against the generator’s actual re-fetch dispatch (dispatchPerformsReFetch) so the single-homed predicate and the emitter cannot drift, per "validator mirrors classifier invariants". Slice 3 (theSingleRecordTableField→RecordTableFieldcollapse) was split out to R305 once implementation showed it is an emit-mechanism unification, not a leaf merge; R290’s delivered leaf set is 48 (the appendix’s 47 is R305’s post-collapse target). TheChildField→SourceFieldcarrier rename is split to R302. Coverage:ReFetchDerivationTest(behavioural assertions on the accessor and on validation output, nocode().toString()body matches),ConstructorFieldValidationTest(retargeted to the build-time rejection),GeneratorCoverageTest.everyGraphitronFieldLeafHasAKnownDispatchStatus(reflection-driven exhaustive/disjoint dispatch partition, stays green with one fewer entry), the R281/R299 classified corpus (byte-identical modulo the one removedconstructorexample), and the compile + execution tiers against real PostgreSQL as the behavioural backstop. Full reactor green end-to-end. -
R303 (
fbe9ac6+642d67a+3462bf8; docs-hygiene rework6adebd5+7f98c68): Reify inline datafetchers into named<Type>Fetchersmethods. Most generatedDataFetcher`s were emitted as anonymous inline value expressions in `<Type>Type.registerFetchers(lambdas, the R244/R268 arm-switch ternary, the record-walking blocks) or barenew ColumnFetcher<>(column)instantiations, leaving a datafetcher with no named symbol to breakpoint, stack-trace, or look up by field. Now every datafetcher is apublic staticmethod on the corresponding<Type>Fetchersclass and the registration site is uniformly<Type>Fetchers::<field>, for every owning object type (root, table, node, result, nested, connection, edge,@error). Seam:FetcherEmitter’s value-`CodeBlockcontract is replaced by a sealedFetcherBinding(Inline|Reified) so the field-name-to-method-name derivation lives in one place and the registration value and method declaration cannot drift;bindreturnsReifiedcarrying both theMethodSpecand the registration value (a bareFetchers::fieldfor env-dependent reads, ornew LightFetcher<>(Fetchers::field)for source-only reads), andTypeFetcherGenerator.generateTypeSpeccollects the reified method alongside the existing variant switch (method-backed variants returnInline, so no double-emission; the dispatch partition is untouched). The light path is preserved by renaming the generatedColumnFetcher→LightFetcher: it holds aRead<T>source-read SAM (T apply(Object source)) instead of a jOOQField<T>, stays aLightDataFetcher, and wraps the named read so the env-skipping fast path survives while the read gains a per-field symbol (the jOOQ column constant moves from the registration site into the method). TheBatchKeyField-only gate for nested-type fetcher classes widens to "owns any fetcher" via one sharedFetcherEmitter.nestedTypeOwnsFetcherspredicate that both the reference site (FetcherRegistrationsEmitter.nestedBody) and the emit site (TypeFetcherGenerator.collectNestedFetcherClasses) call, closing the two-gate drift. Connection/edge get<Conn>Fetchers/<Edge>Fetchersdelegate classes (ConnectionFetcherClassGenerator) whose thin per-field methods forward to the sharedConnectionHelper(one home for the pagination logic, hand-auditable;totalCountkeeps its SDL-presence gate);@errortypes get<ErrorType>Fetcherswith reifiedpath/messagereads (ErrorTypeFetcherClassGenerator) wired in place of the inline cast-lambdas inGraphitronSchemaClassGenerator. The R244/R268Outcomearm-switch is reified to statement form (if (!(source instanceof Success<?> success)) return null; return …;), the highest-value readability win and exactly the un-breakpointable expression the "Generated code is read and debugged" principle targets. One shape is honestly deferred: the@errorPayloadAccessorerrors field staysInline(PropertyDataFetcher.fetching(name))because reifying it needs a generation-time resolved accessor thatChildField.ErrorsField/Transport.PayloadAccessordo not carry, a classifier change this Spec scoped out; R304 (filed Backlog) carries the classifier-backed reification plus theresolvesViaPropertyDataFetcher/validateOutcomeChildArmSwitchreconciliation and theDataFetcherKind.PROPERTY_FETCHERretirement. Behaviour-preserving relocation: no classifier branch, no validator-mirror consequence. Coverage:FetcherPipelineTestwiring-kind + method-presence assertions (propertyField_onRecordType_reifiesReadMethod,propertyField_onBackedRecord_wrapsAccessorReadInLightFetcher,recordField_onRecordType_reifiesReadMethod,outcomePayload_columnDataField_armSwitchesInlineReadOnSuccessValueflipped toCOLUMN_FETCHER-wrapping-a-method-reference), the per-type "class is emitted" pins for<Conn>/<Edge>/<ErrorType>/no-BatchKeyField-nested classes,TypeSpecAssertions.wiringFormatchingnew LightFetcher, and the compile-spec (sakila-example,<release>17</release>) + execute-spec tiers as the structural and behavioural backstops; noCodeBlock-string-equality assertions on reified method bodies. TwoIn Review → Readycycles of docs-hygiene rework swept the staleColumnFetcherspelling and the inverted "no per-field fetcher method / emitted inline" invariant out ofsrc/mainjavadoc per "Documentation names only live tests/code". Full reactor green end-to-end. -
R284 (
1e2e719In Review follow-up; the original four-site pass predates distinct history, folded into the squashed trunk): Fix reversed source/target alias order in bridging-hop@referenceConditionJoinemission. An FK-first-hop-then-@conditionbridging path emitted the two-arg condition-method call as(targetAlias, sourceAlias), violating R16’s fixed(srcAlias, tgtAlias)convention; with the documented opptaksamordnaOrganisasjonershape (concrete, mutually incompatible junction-vs-leaf jOOQ table types) the generated resolver fails to compile. The same reversed call was duplicated across five emission sites; the initial pass swapped four (InlineColumnReferenceFieldEmitter,InlineTableFieldEmitter, andSplitRowsMethodEmitter’s split-rows + connection-rows arms) and the In Review follow-up swapped the fifth, `InlineLookupTableFieldEmitter, which carried the byte-identical reversed arm and shipped unguarded. The defect shipped silently because every prior condition-join fixture declared genericTable<?>parameters, which compile either way. Guard: the newReferencePathConditionFixtures.filmActorJunctionToActor(FilmActor, Actor)fixture takes concrete incompatible types, so any future re-reversal fails to compile in compile-spec. Coverage: executionGraphQLQueryTest.splitTableField_bridgingConditionJoin_returnsActorsPerFilmround-tripsFilm.actorsViaJunctionCondition(split-rows path); inline-lookup guardFilmInlineBundle.actorsByKeyViaJunctionConditionroutes the same FK-then-bridging-@conditionpath throughInlineLookupTableFieldEmitter(the fifth site). Full reactor green. -
e6d213d(reframe) +cc18815(impl) →5e34fb7(Spec → Ready) ; R299 (intention-classification-dimension): Forward note (R316, 2026-06-19): thecarrier x intent x mappingmodel this entry migrated the corpus onto was itself pivoted to(source, operation, target); the@classifieddirective andDimensionTuplenow carry the new axes. Read the vocabulary here as historical. migrate the R281 corpus from the two-axis(producer, mapping)verdict onto R222’s refinedcarrier x intent x mappingmodel, while the leaves are still intact and ahead of R290’s field-side materialisation.DimensionTuplebecomes(carrier, intent, mapping);ProducerStepretires; newCarrier {Query, Mutation, Source}and the full-modelIntentenum land, mirrored SDL-side inClassifiedDsl.PRELUDEand checked bycarrierMirrorsAdapterValues/intentMirrorsAdapterValues.LeafTupleAdapterreconstructs all three from leaf identity (carrier from the enclosing sealed type, intent from leaf +DmlKind, mapping as before); the switch stays exhaustive overOutputField, and the derived layer (FetchRelated/ re-fetch / new-query / polarity) stays computed, never asserted.@classifiedmigrates to(carrier:, intent:, mapping:)across every corpus fixture;everyDimensionValueIsExercisednow coversCarrier+Intentwith a known-gap allowlist (the five R222 model-completeness gaps plus upstream-rejectedUpsert, mirroringNO_CASE_REQUIRED). `code-generation-triggers.adoc’s Field Classification section is rewritten to the three axes + derived layer + assert-vs-derive, the child-table / record-table examples now teaching the derived layer. Corpus-and-docs only: no generator, validator, or field-model change (those are R290). -
fc03387+6ab5127+cf8262e(impl) →97fbc02(In Review) ; R293 (build-warning-cleanup): clean up build-time warnings so a fullmvn install -Plocal-dbis warning-free under-Xlint:all -Werror, leaving only declared-out-of-scope environment lines (sandbox jOOQ PG-version mismatch, Maven’s own Guice/UnsafeJVM notes, the R294BuildWarning-channel fixture advisory). Mechanical sweep (handwritten-source raw types / dangling javadoc /serialVersionUID/getType→getTypeOrNull/Charsets→StandardCharsets, lsp FFM@SuppressWarnings("restricted")+ surefire--enable-native-access, maven-plugin descriptor link,junit-platform.propertiestest-jar exclusion) plus emitter fixes. The generated-code casts the spec slotted for narrowest-scope@SuppressWarningswere instead dropped:env.getArgument/env.getSourceare<T> T, so a typed-LHS statement removes the cast via inference (the spec’s preferred step-1 over its mis-categorised example);@SuppressWarningsreserved for the genuinely-unchecked residuals ((List<X>) map.get(key)offMap<?,?>).cf8262efurther replaced the record-carrierOutcome.Success<?>capture + uncheckedsuccess.value()cast with a checkedinstanceof Outcome.Success<element>pattern-match (Success<T> implements Outcome<T>), via a sharedemitRecordSourceLocalhelper. Guard:-Werroradded to the parent pom’s globalcompilerArgs(every-Xlint:allcategory enforced, none excluded, documented escape hatch), inherited by sakila-example’s release-17 generated-source compile (the cross-module backstop); ratchet comment updated. jOOQ ambiguous keys resolved by disabling<implicitJoinPathsToMany>on thepublic.*codegen (no catalog consumer navigates those to-many path methods). -
2228b67…aa7a45e; R281 (classification-test-dsl): classification test DSL,@classifiedspec-by-example. Replaces the doc-prose-plus-405-enum-row double specification of classification behaviour with an annotated SDL corpus that is the readable spec. Two test-only directives,@classified(producer: [ProducerStep!]!, mapping: Mapping!)on output fields and@classifiedType(as: TypeVerdict!)on types (enumsProducerStep/Mapping/TypeVerdictvalidated SDL-side viaClassifiedDsl.PRELUDE, never leaked into the productiondirectives.graphqls), assert the two-axis dimensional verdict R222’s field pivot will adopt:producer(a pipeline of length ≤ 2:∅inline-correlate, or stepsQuery/Service/Dml) ×mapping(Table/TableConnection/Column/Record/Field). The throwawayLeafTupleAdapterbridges today’s fused sealed leaves to those tuples via a compiler-exhaustive switch overOutputFieldthat is R164’s leaf↔dimension truth table.ClassifiedHarnessclassifies each fixture with today’s classifier and compares;ClassifiedDslTestpins three coverage obligations (adapter totality compiler-enforced, every dimension value exercised,TypeVerdictmirrorsGraphitronType’s non-failure leaves with a simple-name-uniqueness guard). `VariantCoverageTestwas rewired so output-field and non-failureGraphitronTypeleaves are owned byClassifiedCorpus.coveredLeaves()as the single source of truth, while input-field leaves stay on theGraphitronSchemaBuilderTestenum table and the failure leaves stay out of scope.QueryViewRendererrenders doc examples as query/fragment-as-view projections (real SDL regenerated, test directives stripped) with input-object and abstract-output-type closure expansion (pre-migration hardening item 3,QueryViewRendererTest);code-generation-triggers.adocrenders its worked examples from the corpus (ClassifiedDocTest) with reference tables corrected againstTypeFetcherGenerator’s four-way emission partition (only `CompositeColumnReferenceFielddeferred). Retirement inventory committed atroadmap/audits/classification-test-dsl-inventory.md(35 pure-verdict rows, all retired against a corpus coordinate). Drives R222 Stage 3 (field-side pivot) as its executable acceptance spec; theTableInterfaceField/TableMethodFieldper-parent-query N+1 defect was filed as R288 rather than blessed as a[Query]verdict. Full reactor green. -
fae7c6f+1fdcf18; R295 (connection-synthesis-inherits-federation-tags): synthesised Connection / Edge / PageInfo types now inherit the federation@tagapplications of their@asConnectioncarrier field, closing the contract-composition break where a tag-filtered contract kept the carrier field but dropped its untagged return type.ConnectionPromotercollects the arm-appropriate tags (carrier field on the directive arm, SDL Connection type on the structural arm), applies them to the synthesised Connection/Edge schema forms beside the existingshareablearm, and folds a tag union across all promoted carriers into the synthesised PageInfo exactly aspageInfoShareablefoldsshareable; an author-declared PageInfo is left untouched. Carriers sharing oneconnectionName:union their tags into the already-registered entry viatypeRegistry.enrichon a transformedschemaType()(no paralleltagsrecord component, per Model metadata over parallel type systems). Tests:ConnectionPromoterTest(explicit, repeatable, shared-name union, structural arm, SDL-PageInfo negative pin) +ConnectionFederationTagPipelineTest(<schemaInput tag>vialoadAttributedRegistryand a federation-SDL emission round-trip). The deferredshareableboolean collapse is filed as R297. Tags land at the type level only; whether type-level-only tags satisfy a real Apollo contract build (vs. the field-level tags legacy contracts validated against) is the outstanding first-client check, tracked as R298 ; it could not run in the implementation/review sandbox, and the green SDL round-trip proves emission, not composition. -
5fa830e+32d7e0d+77573c4(red tests24387b2, comment refresh905f9ef) ; R275 (source-record-carrier-service-error-channel): error channel and data projection for source-record-carrier@servicemutations, reopened-scope completion. The earlier as-built slice (2026-06-05) closed only the to-one, non-@splitQuerycarrier ({ entity: Table, errors }projected offOutcome.Success.value(), bucket Cerrors: nullon the success arm, theNonNullableErrorsFieldrejection); this completion covers the two data-field shapes theopptak-subgraphsaksbehandling mutations actually use, both of which previously emitted an invalid assembled schema (atypeRefto a dropped payload type,graphql.AssertException: type X not found in schema). Slice 1 (5fa830e):@splitQuery-list carriers ({ saker: [Sak!] @splitQuery, errors }) are admitted via the tolerantBuildContext.scanStructuralServiceCarrierPayload(the data field’s PK-keyed follow-up SELECT makes@splitQueryredundant, fired as the establishedwarnIfSplitQueryOnRecordParentadvisory), classifyingSingleRecordTableFieldMANY over theOUTCOME_SUCCESSenvelope; and a recognized-but-unbound orphan carrier becomes a loudUnclassifiedFieldat the mutation-field edge. Slice 2 (32d7e0d, requirement 2):@nodeId-from-record support ({ taggId: ID @nodeId, errors }/{ tagger: [ID] @nodeId, errors }over a service returning the deleted record(s)) encodes node ids straight offOutcome.Success.value()’s in-memory record(s) with no follow-up SELECT, deletion-safe by construction; structural scan grew a named `CarrierFamilyaxis (DML vs SERVICE) carrying the forbidden-directive set and the ID-wrapper policy (SERVICE admits the[ID]list-of-nullable the opptak schema declares), the lockstepresolveDeleteIdEncoder+classifyDeleteIdEncoderErrorpair collapsed into one sealedIdEncoderResolutionresolver feeding both DELETE and SERVICE diagnostics, and a new sealed leafChildField.SingleRecordIdField(wired through every sealed-coverage site: validator,TypeFetcherGenerator.IMPLEMENTED_LEAVES,FieldClassification.SingleRecordId,LeafTupleAdapter, LSP hover/label). Slice 3 (77573c4, requirement 1): the shape-agnosticGraphitronSchemaBuilder.rejectDanglingTypeReferencesbuilder pass demotes any classified field whose SDL Object return element never registered toUnclassifiedField(fails the build and removes the field from emission), closing the residual hole for errors-only and scan-Rejectorphans that the per-shape guard left open; the seven historically-lax arg-mapping fixtures were fixed (not allowlisted) by backingFilmDetailswith a realTestFilmDetailsDto. Coverage: pipelineSingleRecordTableFieldServiceProducerPipelineTest, schema-builderGraphitronSchemaBuilderTestRootFieldCase+UnclassifiedFieldCaserows, executionGraphQLQueryTestdelete-shaped fixtures whose producers synthesize records with ids absent from the DB (9001/9002) so the encoded node ids prove the no-re-fetch contract structurally; no generated-body string assertions. In Review → Done gate (reviewer session ≠ implementer session017DpiWem9o8HCVkgDf7ae5a): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25 across all modules. -
b2c0895; R291 (strip-internal-directives-from-published-sdl): the published SDL (schema.graphqls, both federation and plain arms) no longer carries Graphitron-internal directive definitions/applications or their supporting types. Two-tier support-type model derived fromdirectives.graphqls(DirectiveSupportTypes): the published tier (SortDirection, now with SDL descriptions) classifies iff a non-support coordinate references it; the strictly internal tier never classifies, and a consumer reference to one rejects the referencing type with a typedAuthorError. The retention decision isschema.types()membership, consumed by both the runtime registration and theSchemaSdlEmitterprint seam. Status flip and R253 closure at80623b0. -
b2c0895; R253 (pipeline-runtime-sdl-parity-test): closed as subsumed by R291 (strip-internal-directives-from-published-sdl), which implemented R253’s Route 3 at theSchemaSdlEmitterprint seam (survivor-filtered directive definitions/applications on both arms,generateServiceSDLV2-mirroring federation printer) and re-enabledFederationBuildSmokeTest.emittedSdlMatchesRuntimeSchemaas the pinning parity assertion. One changelog line records both IDs; R291’s own entry lands when it reaches Done. -
ecdc7c4; R186 (nested-input-types-in-mutation-fields): a plain (non-@table) input object grouping columns of the surrounding@tableinput is now admitted on@mutationfields, flattening onto that one table instead of being structurally rejected. The grouping is a wire-format ergonomics shape with no DML semantics; the three structural rejections it replaced (UpdateRowsWalker/DeleteRowsWalkerUnsupportedInputFieldShape,MutationInputResolver’s R128-attributed `NestingFieldarm) are gone. Both walkers flatten aNestingFieldinto its leaf carriers in place and the INSERT resolver recurses its leaves under the same per-field rules; each nested leaf’s wire concern rides on aCallSiteExtraction.NestedInputFieldaccess path so the flat-leaf partition (UpdateRows.setColumns/keyColumns,DeleteRows.whereColumns,TableInputArg.lookupKeyFields) stays flat and the emitters descend the wire map. The emit honors the same absent-vs-null contract at every nesting layer that top-level mutation inputs do (absent / null group skips its subtree; a present group descends per leaf), proved on real PostgreSQL across INSERT, single + bulk UPDATE, and DELETE. List-typed nestings and nested-group@conditionare rejected naming R186; nested@nodeIdFK-targets (R189) compose; nested@tableinputs that introduce a second DML target remain R122’s territory. NewDML_INSERT_NESTING_OKflips the formerDML_NESTING_FIELD_DEFERRED. Single-segment access paths emit byte-identically to pre-R186. -
57cb7b0+f1ee7a6; R266 (deleterows-walker-carrier): DELETE mutations onto theDeleteRowswalker carrier (sealedIdentified | Broadcast), mirroring R246/R258’s UPDATE work for the DELETE verb. Row identification is catalog-derived PK-or-UK coverage via the sharedMatchedKeys.firstCoveredmatcher both walkers call (the seam a futureLookupRowscarrier grows from);Identified’s matched key is a single-row guard, `multiRow: trueopts into theBroadcastarm. NewDeleteRowsFieldworn by the migratedMutationDeleteTableField(dropstableInputArg) plus the newMutationDeletePayloadField/MutationBulkDeletePayloadField;MutationDmlRecordFieldnarrowed to{INSERT, UPSERT}andMutationBulkDmlRecordFieldto{INSERT}(compact-ctors reject DELETE). NewDeleteRowsErrorsub-seal (NoUniqueKeyCoverage,UnsupportedInputFieldShape,OverrideConditionNotSupported) undergraphitron.delete-rows.*. Carving DELETE offMutationInputResolver.resolveInputretired the@valuedirective entirely (absorbing R188): the declaration,DIR_VALUE,DmlKind.acceptsValueMarker/requiresPkCoverage, thevalueMarkedNamespartition machinery, andvalue.adocare all deleted;mutation.adocrewritten to the catalog-derived rule. Rework (f1ee7a6) closed the In Review feedback: aligned theBuildContext@lookupKeyrejection message with theFieldBuildertwin, and shipped the execution-tier UK-covering single-row delete over a dedicated public-schemastorage_binfixture (deleteStorageBinByCode, WHERE on the UNIQUEcode/ RETURNING thebin_idPK, round-tripped against Postgres). -
R8 (
docs-as-index-into-tests, superseded by R279): closed as superseded wholesale rather than shipped independently. Steps 1-2 (re-sectioning groundwork, description normalisation) shipped earlier onclaude/review-docs-plan-adYJW; step 5 was already retired by the variant-coverage meta-test (GeneratorCoverageTest.everyGraphitronFieldLeafHasAKnownDispatchStatus+VariantCoverageTest.everySealedLeafHasAClassificationCase). The remaining steps 3-4, positioningcode-generation-triggers.adocas a map into theGraphitronSchemaBuilderTesttruth table, were deferred until the sealed hierarchy stabilised; that stabilisation is R279’s (field-first-classification-driver) own deliverable; the doc-as-index work (thecode-generation-triggers.adocabsorption) landed in R279’s slice 0 and has since moved to R281 (classification-test-dsl), which now owns it and captures R8 by reference. Discarded per the workflow’s superseded-wholesale rule (the successor spec captures the predecessor); file deleted in this commit. Mirrors the R166 retirement precedent. -
R259 (primary surface bundled in
048c9c7; sibling-namespace slice this commit): the@reference(key:)"did you mean" FK candidate hint is now scoped to the structurally relevant FKs and rendered in the namespace the author typed. The bug: the hint was built fromcatalog.allForeignKeySqlNames()ranked by global Levenshtein distance, so on a large schema the nearest five were dominated by unrelated FKs sharing a token, and an author who wrote keys in the jOOQ Java-constantTABLECONSTRAINTnamespace got suggestions back in the bare SQL-constraint namespace. The primary surface,BuildContext.parsePathElementvia the newfkCandidateNames(sourceSqlTable, attempt)helper (BuildContext.java:897), shipped both fixes: it scopes candidates toJooqCatalog.foreignKeysTouchingTable(…)(new helper,JooqCatalog.java:270, with a global fallback when the source table has no touching FKs) and switches namespace onattempt.contains(""), renderingfkJavaConstantNamevs SQL names. This close adds the cheap half of the sibling surface,BuildContext.unknownForeignKeyRejection(:1009, the@reference(key:)/@nodeIdsynthesis miss path): it now mirrors the same-namespace detection, drawing fromallForeignKeyConstantNames()vsallForeignKeySqlNames(), so both surfaces read in the author’s namespace. The harder half, scoping that sibling (its call sites pass only the FK name, no source table; scoping needs a table threaded through:1115/:1347/:1376/:1889), is filed as R282 (fk-key-hint-sibling-scope). Pinned byJooqCatalogMultiSchemaTest.unknownForeignKeyRejection_mirrorsAuthorFkNamespace_inCandidateHint: against the nodeid fixture’s real FK, a bare-form attempt yields SQL-namespace candidates (no) and a-form attempt yields constant-namespace candidates (carry), the latter failing under the pre-R259 always-SQL behaviour. Out of scope (per spec): LSP completion/hover arms and the FK-resolution logic itself. Fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25. (Gate note: the primary surface was implemented by a prior session and reviewed independently here; the small sibling-namespace mirror was authored in this session, so that slice is self-reviewed, mechanically mirroring the already-reviewedfkCandidateNamespattern with the new test and the full build as the safety net.) -
R254 (
048c9c7, bundled): generatedGraphitronSchema.javaand every per-type*Type.javaare now emitted as flat statement-per-element bodies (oneschemaBuilder/blocal plus one short statement per root type, additional type, scalar, directive, field, interface, possible-type, and applied directive) instead of a single fluent method-call chain whose depth scaled with schema size. The deep chain overflowedjavac’s expression-attribution recursion during incremental compilation under `quarkus:devon large schemas; flattening removes the unbounded depth. Non-trivial sub-values (synthesised scalars, directive definitions, applied directives, field/argument definitions) are factored intoprivate staticfactory methods viaHelperMethodSink, so emission sites are bare-name references.GraphitronSchemaClassGenerator.generate(:204-235) andObjectTypeGenerator(object:135-148, interface:175-185, union:203-212, field-def sub-chain:240-269) carry the cutover; theAppliedDirectiveEmitter/ directive-definition siblings were already statement-form. Pinned by@PipelineTierSchemaEmissionChainDepthPipelineTest: amaxChainDepthscanner asserts no emitted expression-statement exceeds depth 16 across a federation fixture and a deliberately oversizedLARGE_SDL, with a scanner self-test (maxChainDepth_detectsLongChainbuilds a 21-segment chain and asserts it scores > 16, plus flat-statement = depth-1 cases) proving the bound would trip on a revert to the chained form;statementCountInGraphitronSchemaBuildBody_scalesWithSchemaSizepins growth. The bound is scanned at string level, not asserted as code-string equality on method bodies, with the carve-out documented per design principles. In Review → Done gate (reviewer session != implementer): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25. (Provenance: the implementation reached trunk bundled inside048c9c7, whose message readsR244 Ready → In Progress; the granular R254 stage commits were squashed in a rebase and are not reachable from trunk.) -
R255 (
048c9c7, bundled): fix duplicate column projection in generated$fields()methods (the RC-6 regression). When a type’s composite@node(keyColumns:)overlapped a sibling@fieldColumnFieldon the same column (forced by federation@key+@overrideentity dispatch always selectingid), both classifier arms appended the same jOOQTableField, projecting the column twice and spamming jOOQ "Ambiguous match" INFO logs on every fetched row.TypeClassGenerator’s `$fields()accumulator is now aLinkedHashSet<Field<?>>(:215) that dedupes by jOOQFieldidentity while preserving projection order, returned as aListvia anArrayListwrap (:236) so the emitted surface is unchanged; the formerif (!fields.contains(…))guard collapses to a plainaddunder Set semantics (:227-234). Aliased.as(name)projections stay distinct (jOOQ caches oneTableFieldper aliasedTable), so only true duplicates fold. Pinned non-vacuously at the execution tier byFederationEntitiesDispatchTest(:435-464): a federated_entitiesquery selecting bothidandcustomerId(both →customer_id) captures the emitted SQL and assertscustomer_idappears in the projection exactly once, which fails under the oldArrayListaccumulator;@PipelineTierDedupeReferenceProjectionPipelineTestpins the classifier precondition (compositeCompositeColumnField+ siblingColumnFieldover the same column). In Review → Done gate (reviewer session != implementer): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25. (Provenance: granular R255 stage commits were squashed in a rebase; the code reached trunk bundled inside048c9c7.) -
R260 (
143b0c13): make generated NodeId-decode code readable and debuggable. Every NodeId-decoded condition argument (all key arities, skip and throw, scalar and list) now lifts into aprivate static decode<Type>Key/Keys/Row/Rowshelper on the<Root>Conditionsclass viaCompositeDecodeHelperRegistry, so the call site collapses tohelper(wireExpr)(ArgCallEmitter.buildNodeIdDecodeExtraction,:360-389) and the helper body is statement form with meaningful locals (nodeId,key) instead of the former inline nested ternary with underscore pattern-locals and aSupplier-lambda-throw trick (CompositeDecodeHelperRegistry.buildHelper,:86-138). The sibling map/list traversal walkers were converted tomap1/list2/elem3bindings, and a registry-less decode now throwsIllegalStateExceptionrather than falling back to the old inline form (:378). Pinned byCompositeDecodeHelperRegistryTest(naming matrix, return types, projection and skip/throw bodies, including arity-1 coverage) and end-to-end by execution-tierGraphQLQueryTest.films_filteredByArgNodeId_dropsWrongTypeIdViaSkipHelper, which confirms a wrong-type id decodes to null and is dropped by the lifted helper’sfilter(nonNull). The registry unit tests assert on helper-body substrings, defensible for an inherently emitted-shape item since behaviour is pinned separately at the execution tier and the assertions lean on structural API (helper.name()/returnType()/registry.emit()size). Out of scope (correctly excluded): the lookup-key and R195 input-bean decode-local paths. In Review → Done gate (reviewer session != implementer session0182HAPCJwMRxopaPWwquKyk): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25. -
R64 (retired as obsolete + residual dead-code deletion): the planned lift, making
SplitRowsMethodEmitter.buildRuntimeStubaccept a typedRejection.Deferred/EmitBlockReasoninstead of a free-formString reason, is moot. The premise was thatSplitRowsMethodEmitter.unsupportedReasonreturnedOptional<Rejection.Deferred>and fourbuildFor*callers fed.message()intobuildRuntimeStub; the rows-method rework that followed (thebuildSingle/buildList/buildConnectioncutover) deletedunsupportedReasonand the four.message()call sites entirely, leavingbuildRuntimeStubas a private, uncalled method (verified: zero call sites repo-wide, nounsupportedReason/Rejection.Deferred/EmitBlockReasonreference anywhere in the file). There is nothing left to type-lift. This entry deletes that dead method (and its now-empty// Stubssection header), the actionable residue the obsolete plan left behind. Fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25 across all tiers after the deletion. (Closure framing: retirement of an obsolete Backlog item plus a verified dead-code removal, not an In Review → Done approval of planned work; mirrors the R166 retirement precedent.) -
R227 (
048c9c7, bundled; retroactive close):mdBodyToAdoctranslates markdown tables embedded in.mdroadmap plans into AsciiDoc|===blocks when staging plan bodies into the documentation site, closing the render-side hole R223 explicitly deferred (R223 only flagged the pattern in authored.adoc). The converter detects a markdown-table block (aMD_TABLE_ROWheader line immediately followed by aMD_TABLE_SEPseparator, then body rows until the first non-table line) and emits[cols="N*", options="header"]+|===, one cell per line.parseMdTableCellsstrips the conventional leading/trailing pipes, splits on unescaped pipes, unescapes\|, and leaves pipes inside backtick code spans intact (sosurvives); each cell runs through the same bold / link / em-dash-sweep transforms as body prose, with literal pipes re-escaped for AsciiDoc. Coverage:Map<K|V>MdTableToAdocTest(7 cases) pins the simple conversion +cols="2*"synthesis, in-cell bold/xref transforms, em-dash sweep, the backtick-pipe-protection corner, the code-fence non-conversion skip, and theparseMdTableCellsstrip/unescape edge cases. Scope cut from the spec: the[cols=…]attribute is synthesized from the column count only (equal-widthN*); GFM alignment markers (:---:) are parsed by the separator regex but not carried into per-column alignment, since no roadmap.mdtable uses them and equal-width is the safe container-filling default. Provenance note: the implementation andMdTableToAdocTestwere committed bundled inside048c9c7(whose message readsR244 Ready → In Progress) and the item never transitioned out ofBacklog; this entry records the retroactive In Review → Done close. Gate (reviewer session != implementer session01YQEc4FG3cqf18pHwgVAnsX): implementation and test verified present in-tree and green on trunk. -
R166 (Backlog, never specced; retired without a landing commit):
graphqlschemavisitor-driven-emissionsuperseded by R279 (field-first-classification-driver). R166 proposed aGraphQLSchemaVisitor-driven emission walk to fix per-emitter skip-filter drift (the R165 bug class) and the missing reachability sweep; R279 delivers both at classification time (the prune is structural, emission stays plain iteration over the prunedGraphitronSchema), so the visitor-driven emitter and R166’s standaloneReachabilityPruneralternative are both unneeded. R166’s emission-side open questions (cross-cutting aggregators, utility-class emitters, visitor ordering/determinism, visitor test ergonomics) evaporate with emission staying iteration-based. The one orthogonal sub-thread, the typed non-empty carrier forFetcherRegistrationsEmitter.emit’s `Map<String, CodeBlock>return (R166 Q7, originally R165), spun out to R280 (fetcher-bodies-nonempty-carrier). -
R276 (
468b86e+8498e61+4ad9030+19681e5+9f53f36+ spec correctionc69bce5): Record binding is reflection-only and sound. The four classifier/binding sites that still read@recordto drive backing-class or kind are gone:RecordBindingResolver.groundServiceField’s `sdlHasRecordgate,TypeBuilder.classifyType’s `|| hasAppliedDirective(DIR_RECORD)arm, and the directive-classNamefallbacks inbuildResultType/buildInputType. The service producer’s reflected return element now grounds the result observation through a sharedgroundProducerResulthelper under a cardinality-match guard (single→single, list→list) plus the@table-backed-SDL andshouldBindguards, so a source-record-carrier payload (no@record) binds to its producer’sJooqTableRecordTyperather than degrading to an unbound plain object; the R75 list-carrier path is preserved.@recordstays a parseable, registered directive with the ignored-directive warning (emitDirectiveIgnoredWarnings/readRecordClassName), so existing schemas keep loading. D1:@recorddropped fromdetectTypeDirectiveConflict(only@tablevs@errorremain mutually exclusive;@table/@error+@recordwarn instead of reject). The reopened completion scope made binding complete and sound:groundComputedFieldgrounds@externalField/ChildField.ComputedFieldthrough the same shared helper, andpropagateAccessorChainsfolds-then-cascades parent-accessor bindings (the root fix for theFilmCardWrapper.film/RecordExample.fieldCexecution regressions).GraphitronType.PlainObjectTypeis eliminated as a terminal classification (Javadoc mention only): a genuinely unbound reachable object is routed toUnclassifiedType/ left absent and surfaces at the field edge asUnclassifiedField(build-time failure, no silent runtime null), with the double-classification guard hardened.GraphitronType.PojoResultType.NoBackingandTypeClassification.UnbackedPojoResultare deleted (PojoResultTypecollapses toBacked), with every consumer updated. The spec-correction commit reverses a conflation: the LSP backing-shape projectionTypeBackingShape.NoBacking.UnbackedResultis kept (it is load-bearing for eleven still-liveGraphitronTypevariants inCatalogBuilder.projectTypeplus three LSP tests); only its stale Javadoc was refreshed. R157’s two LSP backing-shape fixtures migrated from@record(className:)to@serviceproducers. Carve-outs: R277 (@tableMethodunder a table-boundNestingField) filed to Backlog with its execution test@Disabled; R275 consumes R276’s carrier binding; thePlainObjectType/NoBackingdeletions are the subtractive slice R222’s hierarchy cleanup builds on (boundary confirmed non-colliding, R222 still in Spec). Coverage: pipelineR96RecordBindingPipelineTest.unreachable_recordTypeIsIgnored_leftUnclassified+serviceListCarrier_bindsWrapperToJooqTableRecord,SingleRecordPayloadPipelineTestorphan-carrier case,R157PipelineTestLSP migration, the migratedGraphitronSchemaBuilderTestproducer cases; no generated-body string assertions. In Review → Done gate (reviewer session ≠ implementer; widened scope re-confirmed at this gate per the spec’s own allowance): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25 across all eleven modules. -
R214 (
048c9c7, bundled; retroactive close): inferargMappingwhen a@condition/@service/@tableMethodJava signature is unambiguous, so authors no longer have to rename a Java parameter or write a redundantargMapping: "javaName: gqlName"for a structurally-unique pairing.ServiceCatalog.inferBindingsByTypeaugments the name-basedargByJavaNamemap after the override-typo check in two layered branches: arity-unique (exactly one unbound Java parameter and one unclaimed GraphQL slot, bound positionally only when the slot has no canonical Java mapping (named input object / enum) and the parameter is not a canonical scalar, so theunambiguousReachablePathdot-path suggestion still wins the scalar-into-wrapper case) and type-unique (each Java type appearing exactly once among unbound parameters and once among unclaimed slots is paired; asymmetric counts stay unbound and the existing diagnostic fires).Table<?>,DSLContext, context-key-named, and SOURCES-shape parameters (couldBeSourcesShape:List<RowN>/List<RecordN>/List<TableRecord>andSet<>equivalents) are held out of the candidate set so the per-parameter SOURCES classifier still wins at child coordinates. Threaded through every reflection caller that has slot types in scope (ConditionResolver,TableMethodDirectiveResolver,ServiceDirectiveResolver, and input-field@conditioninBuildContext); the path-step@conditioninresolveConditionRefhas no slot types and is unaffected. Coverage:TestConditionStub.argConditionTypeUnique/argConditionTwoStringsfixtures plusServiceCatalogTest.reflectTableMethod_typeUniqueSignature_infersBindingWithoutArgMapping(pins the inferredParamSource.Argbinding,whatever→ argopptaksNavn) andreflectTableMethod_typeAmbiguousSignature_fallsBackToNameMatchingDiagnostic(pins the floor). Open follow-ups remain live items: inferred-binding provenance for the resolved-coordinate report / LSP (R218) and unifying the two branches under aJavaTypeKey-counted rule (R219). Provenance note: the implementation, fixtures, and resolver pass-throughs were committed bundled inside048c9c7(whose message readsR244 Ready → In Progress) and the item never transitioned out ofBacklog; this entry records the retroactive In Review → Done close. Gate (reviewer session != implementer session01YQEc4FG3cqf18pHwgVAnsX): implementation verified present in-tree, and green on trunk through every Done item that landed on top (R244, R246, R195, R271). -
R271 (
07d6c23+ self-reviewabee3f1+e6ec2d5): Retire the-prefixed (dunder) Java locals/params/lambda-vars emitted across the generator (FetcherEmitterr/src/fetched/byPk/ordered/match/key/out/ids;TypeFetcherGeneratorvalidator/violations/vplus thearg_/insertKey/bulkKey/lookupKey/setKey/bulkSetKeybase prefixes;GeneratorUtilselt/k;InputRecordGeneratorc_/e;ChannelEarlyReturnEmitterviolations) 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: theprefix was a lazy default, not a collision guard ; the generator emits every name in scope (signatures included), so a collision is knowable at generation time. The In-Progress audit confirmed no emitter places an author-derived parameter beside a generated local (all dunder locals live in DataFetcher lambdas, batch-loader/helper methods, orfromMapfactories with generator-fixed signatures), so no disambiguation machinery was built. Genuine collision-avoidance names stay: synthetic SQL column aliases (sort/idx/rn/typename/pkN) share the result-set column namespace with consumer-controlled table columns and remain-wrapped, now promoted to named constants where they were bare repeated literals (RN_COLUMN/IDX_COLUMNonMultiTablePolymorphicEmitterandSplitRowsMethodEmitter) and documented with the DB-column-collision rationale at each constant;GraphitronSchemaClassGenerator’s synthetic-column `typenameroutes through a namedTYPENAME_COLUMNwhile the federationentitiesrepresentation-maptypenamestays a literal (the GraphQL introspection meta-field, a distinct concept). The staleChannelCatchArmEmitterjavadoc namingt/m(the code emitsmapping/cause) is fixed. The standing rule lands inrewrite-design-principles.adoc. No-regression guard ships in two tiers, keying on the Java-identifier-vs-string-literal discriminator (both mask comments + string/char literals before scanning):@PipelineTierDunderFreeEmissionPipelineTest(in-processTypeSpecscan, non-vacuous by anisNotEmptyassertion) and@CompilationTierGeneratedSourcesLintTest.emittedSourcesHaveNoDunderIdentifiers(full Sakila generated-sources walk with a ≥20-file floor against a vacuous pass). External tokens we do not emit (NODE*,federation*,link*) are out of scope. In Review → Done gate (reviewer session ≠ implementer): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25; both meta-tests run (not skipped) and pass. -
R195 (
7f7cac0v1 + rescope876a621+ composite/listef8fe80+c3e10a3+ type-mismatch gate18f79a2+ rework36712f7→98b5a93): decode@nodeId(typeName:)into jOOQ-Record-typed@serviceinput-bean *member fields instead of miscompiling to a wire-String→Recordcast (the R150/R195ClassCastExceptionfamily).InputBeanResolver.buildInputBeanBodynow branches beforeelse → new Direct()when a member’s element Java type is assignable toorg.jooq.Record: with a resolvable@nodeId(typeName:)it classifies to a newCallSiteExtraction.NodeIdDecodeRecordleaf carrying(encoderClass, typeId, keyColumns, TableRef table, nonNull); otherwise it is a typedResult.Failed, so a jOOQ-record member never falls through toDirectagain.InputBeanInstantiationEmitter(the one reusable emitter) emits per-type concrete helpers (decode<Type>Record(Object) → <Type>Record, deduped by record type, plus adecode<Type>RecordListstream variant for list members) whose body calls the now-publicNodeIdEncoder.decodeValues(typeId, nodeId)then loads values positionally withdecoded.fromArray(values, Tables.<T>.<col>…); one call regardless of key arity, coercing through the column converter, with no throwawayRecordN, nofromMap(intoMap())round-trip, and no deprecated-for-removalDataType.convert(Object)(so no@SuppressWarningsleaks into the consumer’s*Fetchers; the encoder’s owndecode<Type>convert is tracked separately as R267). All shapes ship: single-key, composite-key, scalar, list, and the list-of-composite corner. Loud rejections are malformed-directive-only (no@nodeId, missingtypeName:, unknown NodeType) plus a member-type-vs-@nodeId-@tablemismatch gate (18f79a2) that fails generation rather than emittingTables.<NodeTable>.<col>references for the wrong record.ServiceMethodCallWalkercarries the leaf through the R238ValueShapere-projection unchanged;ArgCallEmittergets an explicit unreachable arm. Coverage: pipelineNodeIdRecordInputBeanPipelineTest(9 tests, structural only ; helper presence by name, signatures, and adecodeRecordLeafwalk down the classified model assertingNodeIdDecodeRecordtypeId/key-column arity/record class/nonNull, plus malformed-directive and type-mismatchRejection.message()cases; nocode().toString()body matches, perrewrite-design-principles.adocline 131); compilation tiergraphitron-sakila-example(single/composite/list/list-of-composite fixtures compile theirfromArrayfield references against the real jOOQ catalog); execution tierGraphQLQueryTest(round-trips each shape against PostgreSQL, plusassignFilmRecord_wrongTypeNodeId_throwsDecodeMismatchpinning the throw-on-mismatch contract behaviourally). Original R195 framing (top-level@serviceparameter that *is a jOOQ record,@field(name:)/@table-on-input translation) deferred (tangled with R97). In Review → Done gate (reviewer session ≠ implementer/prior-reviewer): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25. -
R246 (
8a04c0b+975f593+c63c14c+ reworkf3a39ea): UpdateRows walker carrier (R222 UPDATE slice) with PK-or-UK identification.@mutation(typeName: UPDATE)returning its@tabletype directly (or ID) now classifies throughFieldBuilder.classifyUpdateTableField→UpdateRowsWalkerinstead ofMutationInputResolver.MutationUpdateTableFielddrops itstableInputArgcomponent (andtableInputArg()comes off theDmlTableFieldsealed parent; INSERT/DELETE/UPSERT keep their own) and gains two non-Optional slots via the newUpdateRowsFieldinterface: a slimInputArgRef(SDL arg name, input type name, jOOQTableRef, list flag) built directly by FieldBuilder, and anUpdateRowscarrier (sealed, oneIdentifiedarm whose compact constructor enforces non-emptysetColumns) holding theMatchedKey(PrimaryKey/UniqueKey) plus theSetColumn/KeyColumnpartition. The load-bearing claim is PK-or-UK identification: the walker reads jOOQTable.getPrimaryKey()/getKeys()viaJooqCatalog.candidateKeys(PK-first, deduped), matches the first key whose column set is a subset of the input-covered columns, and partitions input fields into WHERE (matched-key) and SET (everything else) halves.multiRow: trueon UPDATE is rejected outright at the FieldBuilder pre-check (Rejection.deferred, empty slug, no follow-up planned) ; broadcast UPDATE has no replacement path; covering a PK/UK is the single-row UPDATE shape. The emitter cutover is carrier-driven in place (not a separateUpdateRowsEmitterclass):buildMutationUpdateFetcher/buildBulkUpdateFetcherprojectsetColumns()/keyColumns()back into theSetGroup/InputColumnBindingGroupshapes viasetGroupsOf/keyGroupsOf, emitting byte-identical SQL. Error taxonomy ships as a sibling sub-sealUpdateRowsError implements Rejection.AuthorError(five arms:NoUniqueKeyCoverage,NoSetFields,MixedCarrierKeyMembership,UnsupportedInputFieldShape,OverrideConditionNotSupported), each withlspCode()undergraphitron.update-rows.*, wired into the LSP projector +typed-rejection.adoc+RejectionSeverityCoverageTest. R215’s classify-time admission of@condition(override: true)on UPDATE input fields inverts to a typed walker rejection (the filter was never emitted). Absorbs R146 (PK-or-UK coverage, discarded) and R188’s UPDATE-side partition scope; built as a translator over the already-classifiedInputFieldpermits rather than raw-SDL re-derivation (the R238-style substrate concession, follow-up filed as R257). Coverage: unitUpdateRowsWalkerTest(11 cases); pipelineGraphitronSchemaBuilderTest(typed-arm migrations +R246_UPDATE_MULTIROW_TRUE_DEFERRED+R246_UPDATE_ARG_CONDITION_STRUCTURAL_REJECTED+ R215 inversion); execution via the existingupdateFilmround-trip (UK-driven execution case deferred, justified in as-built notes). In Review → Done gate (reviewer session ≠ implementer/prior-reviewer): fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on JDK 25 (graphitron, graphitron-lsp incl.RejectionSeverityCoverageTest, and the execution tier all pass). -
R250 (
73e7670+39072a3):GraphitronSchemaClassGenerator.generatenow emits.withSchemaAppliedDirectives(java.util.List.of(…))on the runtimeschemaBuilder, immediately after theadditionalDirective(survivors)loop and before.codeRegistry(…), so the consumer’sextend schema @link(url:…, import:[…])propagates into the generated runtime build. Pre-R250 the schema-applied list was lost:GraphQLSchema.newSchema()starts empty and.additionalDirective(…)only emits directive definitions, not applications; the symptom in one consumer’s deployment was supergraph composition (@apollo/federation-internalscompleteSubgraphSchema) failing to detect Fed2 because theschema @link(…)block was missing from the runtime SDL, falling through tocompleteFed1SubgraphSchemaand rejecting the canonically Fed2-shaped@keydeclarations with "argument fields should have type_FieldSet!but foundfederationFieldSet!`". `AppliedDirectiveEmitter.applicationsForSchema(GraphQLSchema)is the new entry point; the schema-applied list is not aGraphQLDirectiveContainerin graphql-java so the helper takes the rawGraphQLSchemarather than reusingapplicationsFor. Shape contract diverges from the per-container sibling on purpose and is documented inline:applicationsForreturns blocks pre-wrapped in.withAppliedDirective(…)because the per-type builders take one application at a time;applicationsForSchemareturns bareGraphQLAppliedDirective.newDirective()…build()blocks becauseGraphQLSchema.Builder#withSchemaAppliedDirectivestakes a singleList<GraphQLAppliedDirective>. Survivor filter mirrors the per-container path; generator-only directives are skipped. Argument-value rendering routes through the sameValuesResolver.valueToLiteral+AstPrinter.printAst+Parser.parseValuechain the per-type emitter already uses, so@link’s `import: ["@key", …]round-trips through an AST list literal without per-shape coding. ThelinkImportscalar andlinkPurposeenum referenced by@link’s argument types are already registered on the runtime schema by R248’s `ScalarTypeResolverSynthesised arm and standard enum registration, soemitInputType’s `GraphQLTypeReference.typeRef("linkImport")resolves at schema-build time. Coverage: unit-tierAppliedDirectiveEmitterTest.applicationsForSchema_emitsBlocksForSchemaLevelSurvivorDirectivesandapplicationsForSchema_skipsGeneratorOnlyDirectivespin the helper output for a schema-applied@link(url:…, import:["@key"]); unit-tierGraphitronSchemaClassGeneratorTest.build_emitsWithSchemaAppliedDirectives_forSchemaLevelLinkpins the emitted call site and its position relative to.codeRegistry(…), withbuild_skipsWithSchemaAppliedDirectives_whenNoSchemaLevelSurvivorsas the negative arm; pipeline-tierFederationBuildSmokeTest.serviceSdlExposesSchemaAppliedFederationLinkbuilds the full sakila federated schema, queries_service { sdl }, and asserts the printedschema { … }block carries@link(url:"https://specs.apollo.dev/federation/v2.10", import:["@key"]), locking the round-trip consumer SDL →applicationsForSchema→withSchemaAppliedDirectives→Federation.transform→_service.sdlagainst silent removal. The R247 file-emission side gains a sibling assertion inSchemaSdlEmissionTestthat the federated file artefact carriesschema @link(, closing the loop on the file the supergraph composer actually reads. Out of scope, follow-up filed: multi-federation-@linkconsumer schemas (FederationLinkApplieralready rejects more than one federation@linkwith a developer-readable error); re-evaluating whetherFederation.transform(base).setFederation2(true)is the right runtime wrap (separate concern, entity-resolver wiring is independent); multi-file federation fixture coverage for R247’s file emission (R252, filed). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R247 (
73e7670+39072a3): NewSchemaSdlEmitterruns at the tail ofGraphQLRewriteGenerator.runPipelineand renders the assembledGraphQLSchematotarget/generated-resources/graphitron/<outputPackage as path>/schema.graphqls;GenerateMojoregisters that directory viaproject.addResource(…)so maven-resources-plugin copies it intotarget/classes, shipping the file at<outputPackage as path>/schema.graphqlsin the consumer’s JAR. The federation arm runsFederation.transform(assembled).setFederation2(true).build()beforeServiceSDLPrinter.generateServiceSDLV2, mirroring the consumer’s runtime build so_Service/_entities/_Entityare present on both sides; the non-federation arm uses graphql-java’sSchemaPrinterwithincludeDirectives(true)/includeScalarTypes(true)/includeIntrospectionTypes(false)/includeSchemaDefinition(true).RewriteContextgains anoutputResourcesDirectoryrecord component;AbstractRewriteMojo.resolveOutputResourcesDirectory(basedir)derives it fromproject.getBuild().getDirectory()with abasedir/targetfallback for hand-builtMavenProjecttest fixtures (no@Parameter, no per-consumer toggle); after the self-review followuprunGeneratorreturns theRewriteContextsoGenerateMojo.executereadsoutputDirectoryandoutputResourcesDirectoryfrom one derivation site. Tests: unit-tierSchemaSdlEmitterTest(federation + non-federation + empty-package arms); pipeline-tierSchemaSdlEmissionTest(federated SDL carries the canonical@key, the synthesisedfederation__FieldSetscalar, the@linkdirective declaration, and the schema-applied@link(…)block; non-federation SDL parses throughSchemaParserand is missing the federation surface; classpath resource lookup non-null under bothoutputPackage`s); `GenerateMojoTest.buildContext_derivesResourcesDirectoryFromBuildTargetlocks the Maven-convention path against the hardcoded relative segment. Implementation folded the R250 work in per the user’s "ship together" directive (covered separately on R250’s gate). Out of scope, follow-up filed: the pipeline ↔ runtime SDL parity test (FederationBuildSmokeTest.emittedSdlMatchesRuntimeSchema,SchemaDiffing-based) landed@Disabled; closing the remaining non-survivor directive-definition / -application diff is tracked as R253 (Backlog), with Route 1 / 2 / 3 routes laid out. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R251 (
1c1b425):AppliedDirectiveEmitter.buildApplicationnow skips arguments whosegetArgumentValue().isNotSet()is true rather than feeding theNOT_SETslot intoValuesResolver.valueToLiteral(whichassertShouldNeverHappen`s on it). The reconstructed `GraphQLAppliedDirectivecarries only explicitly-supplied arguments; consumer-side schema build resolves the rest from the directive definition’s declared defaults, matching graphql-java’s own applied-directive round-trip. Coverage: unit-tierAppliedDirectiveEmitterTest.omittedArguments_areSkipped_notRenderedAsNotSetLiteralbuilds@audit(reason: "pii")on a directive declaring bothreasonandticketand assertsticketdoes not appear in the emitted.argument(…)chain. R248 fixed the adjacent directive-definition-side bug; R251 closes the application-side symmetry. -
R248 (
a2b1705):DirectiveDefinitionEmitternow round-trips argument default values (.defaultValueProgrammatic(…)emitted via the sameGraphQLValueEmitter.emitpathObjectTypeGenerator.buildArgumentalready uses for field arguments), sodirective @key(resolvable: Boolean = true)survives the JavaPoet reconstruction with its default intact. The federation-namespace scalar fix is a sub-taxonomy lift onScalarResolution: a new sealedSuccessfulinterface (javaType()accessor) sits between the root andResolved, with a siblingSynthesisedarm carrying(javaType, sdlName, coercingSourceOwner, coercingSourceField)for scalars that have nopublic static final GraphQLScalarTypeconstant on the consumer classpath.ScalarTypeResolver.resolveFederationNamespaceScalarreturnsSynthesised(String.class, "federationFieldSet", _Any, "type")instead of the oldResolved(String, Scalars, "GraphQLString")placeholder;GraphitronType.ScalarType.resolutionwidens fromResolvedtoSuccessful;TypeBuilder’s federation-namespace branch and Java-type registry lookup narrow to `Successful;TypeBuilder.asRejectionswitches onSuccessful(every successful arm throws ; only rejections reach the dispatcher).AppliedDirectiveEmitter.emitInputTypeemitsGraphQLTypeReference.typeRef(name)for federation-namespace scalars instead ofScalars.GraphQLString, so directive-definition and applied-directive argument slots both reference the synthesised scalar by name.GraphitronSchemaClassGenerator.build()’s scalar-registration loop dispatches on the variant: `Resolvedemits the existing.additionalType(Owner.FIELD),Synthesisedemits an inline.additionalType(GraphQLScalarType.newScalar().name(<sdl>).coercing(<owner>.<field>.getCoercing()).build());_Any.type.getCoercing()is the same lever federation-jvm uses inensureFederationV2DirectiveDefinitionsExistwhen synthesising missing federation scalars at the registry+wiring entry point. The misleading "federation-jvmtransform()replaces the placeholder after the base schema is built" comments atScalarTypeResolver.java:83-95/:310-314,TypeBuilder.java:601-605/:647-651, andAppliedDirectiveEmitter.java:122-134retire ;Federation.transform(GraphQLSchema)only adds_Any/_Entity/_Serviceand wires entity resolution, never rewrites@keyor injects scalars; the divergence reached the printed Service SDL untouched. Coverage: pipeline-tierFederationBuildSmokeTest.serviceSdlExposesCanonicalKeyDirectiveShapeasserts the printed SDL carriesdirective @key(fields: federationFieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACEandscalar federationFieldSet(the end-to-end behavior subgraph-composition tooling validates against);DirectiveDefinitionEmitterTestextends the existing argument-emit test with a.defaultValueProgrammatic(+"strict"assertion and adds a Boolean-default arm (@flag(enabled: Boolean = true)) to cover theGraphQLValueEmitter.emitdispatch on a different value shape;ScalarTypeResolverTestflips the federation-namespace resolver test to expectSynthesised(_Any, "type")and adds a second arm onlinkImportto confirm the dispatch isn’t FieldSet-specific;GraphitronSchemaBuilderTestnarrows existingScalarType.resolution()reads to theResolvedarm they’re testing. Out of scope (called out): argument-level@deprecatedon directive definitions (no survivor directive Graphitron emits today carries one); re-emitting@linkitself (the federation library injects@linkinto the registry viaLinkDirectiveProcessor, the survivor walker picks it up like any other directive); switching graphitron’s federation entry point fromFederation.transform(GraphQLSchema)to the registry+wiring overload (would forfeit the prebuilt-programmatic-schema fast-boot model the rewrite chose in R10’s predecessor landing); federation v1 surface (FederationDirectives.key/_FieldSet.type/ensureFederationDirectiveDefinitionsExist) sinceFederationSpec.URLpins v2. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R237 (
96869ab+e24feac+ee9720f+246b349+aa83a16+8845717+18a37a2): Retire the@LoadBearingClassifierCheck/@DependsOnClassifierCheckannotation pair and theLoadBearingGuaranteeAuditTestaudit infrastructure. 183 annotation blocks stripped across 50 Java files; the four annotation classes (LoadBearingClassifierCheck,LoadBearingClassifierChecks,DependsOnClassifierCheck,DependsOnClassifierChecks) and the audit test +auditfixture/package deleted. Phase 2’s four-bucket classification of the ~59 active producer/consumer keys found |c-signal|=0: every cross-module producer-consumer pair was already mechanically pinned by graphitron-lsp tests (FieldCompletionsTest,HoversTest,DiagnosticsTest,DeclarationHoversTest,ValidatorDiagnosticsTest) or by structural type narrowing on the producer side, leaving no signal-bearing contract that needed a test-side replacement. Phase 3 picked Delete on that strength; two follow-up Backlog items track the structural type-system lifts that retire the residual producer-consumer linkages mechanically rather than via documentation: R239 (column-field-requires-table-backed-parent, b-cheap, single-recordparentTablelift toColumnFieldrecord component) and R240 (service-catalog-strict-tablemethod-return+tablemethod-resolver-return-is-table-bound, b-relational, type-token threading onMethodRef.StaticOnly×ReturnTypeRef.TableBoundReturnType). Knock-on:PkResolutionEmitterReachabilityTest.classifyDeleteTableProjectionWearsLoadBearingClassifierCheckPinretired with its annotation dependency (the sibling sealed-arm symmetry test still pins the rejection contract structurally); residual javadoc references rephrased across the rewrite tree in two waves: the initial Phase 5 sweep (aa83a16+8845717) covered 11 main-source files, 4 test files, and 18 roadmap-item plan bodies; a self-review follow-up sweep (18a37a2) caught a further 15 main-source, 6 test, 1 schema, and 1 docs site still carrying the retiredload-bearing classifier check {key}framing ; each rephrased to anchor on the actual structural pin (sealed-variant arm, compact-constructor invariant, non-null record component, named resolver class) rather than the retired key.rewrite-design-principles.adocPhase 1 rewrite shipped at96869abanchored the principle on the three surviving layers (type-system narrowing at producer, pipeline-tier tests, cross-module compile againstgraphitron-sakila-example); theprinciples-architectagent andsrp/reviewer-promptskill rubrics swapped the "Load-bearing classifier checks" rubric for "Missing type-system lift". Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R233 (
01c0172+d02859f): LSP@field(name:)completion + hover arms now resolve the column against the@referencepath’s terminal table instead of the enclosing type’s@table, closing the bug R224 fixed for diagnostics but not for the two sibling LSP surfaces.FieldClassificationgains a sealed nestedLspColumnDispatch(permitsResolve(tableName)/Silent/FallThrough) and an exhaustivelspColumnDispatch()default method that switches over all 30 sealed permits with nodefaultarm ; a new permit fails the switch to compile, forcing one deliberate placement before any consumer-side switch. The four column-bearing permits (Column/ColumnReference/CompositeColumn/CompositeColumnReference) produceResolve(tableName)carrying R224’s already-projected terminal table;InputUnbound/UnclassifiedproduceSilent; every other permit producesFallThrough. The three consumer sites (Diagnostics.validateFieldMember,FieldCompletions.completionsFor,Hovers.columnHover) collapse to a uniform 3-arm switch on the projection:ResolveandSilentreturn directly,FallThroughdrops through to the existing backing-driven dispatch.FieldCompletionsswitches fromTypeContext.enclosingFieldDefinitiontoenclosingFieldOrInputValueDefinition(R224’s helper) so input-sideinput_value_definitionnodes resolve too;Hovers.columnHoveradopts the same helper. A smallmergeWithSigilhelper inFieldCompletionsshares the$source-sigil merge between the new dispatched arm and the existing backing arm. Annotation hygiene:field-classification-payload-faithfulnow has five consumer sites (InlayHints.compute,DeclarationHovers.compute,Diagnostics.validateFieldMember,FieldCompletions.completionsFor,Hovers.columnHover); the producer description atCatalogBuilderenumerates the five consumers and nameslspColumnDispatch()as the routing primitive. Tests: pipeline-tierLspColumnDispatchProjectionTestdrives the full classifier on a synthetic schema and pins the three arms (Resolve / Silent / FallThrough) plus a cross-permit invariant thatColumnReference.tableName() == Resolve.tableName();FieldCompletionsTestgains three R233 regressions parallel to R224’sDiagnosticsTestcases (inputTableWithReferencePathCompletesTerminalTableColumns,outputTableWithReferencePathCompletesTerminalTableColumns,unresolvedReferencePathCompletionSilentOnLspSide);HoversTestgains three symmetric regressions (inputTableWithReferencePathHoversOnTerminalTableColumn,outputTableWithReferencePathHoversOnTerminalTableColumn,unresolvedReferencePathHoverSilentOnLspSide); R224’s threeDiagnosticsTestregressions stay green untouched. Self-review cleanup (d02859f) inverted the spec’s double-Optional-of-Optionaldispatch shape at the two new consumer sites to direct returns (ResolveandSilenteach return directly,FallThroughdrops through), preserving the exhaustiveness guarantee while removing the nested generic. Out of scope (called out, filed as R236): the runtime-sideBuildContext.classifyInputFieldInternalcandidate hint atBuildContext.java:1673draws its "Did you mean" suggestions from the path-origin table rather than the terminal table ; different surface (compile-time validator message vs. interactive LSP) and audience, owned by a sibling Backlog item. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R232 + R129 absorption (
3079b99+001eee4+41e20a3+4bb9d4f+70152d3+8effcbf):@reference(path: [{condition: {…}}])paths now classify and emit a real correlated subquery / split-rows SELECT, replacing the build-time deferred-rejection that previously short-circuited the six condition-join-affectedChildFieldvariants (TableField,LookupTableField,SplitTableField,SplitLookupTableField,RecordTableField,RecordLookupTableField) plus the seventh siblingColumnReferenceField.JoinStep.ConditionJoin’s record header gains a `TableRef targetTableresolved at parse time byBuildContext.resolveConditionJoinTarget; terminal hop from the carrier field’s return-type@tablebinding, intermediate hop by reflecting on the condition method’s second parameter type viaJooqCatalog.findTableByClass; with a compact-constructor null-check as the structural safety net behind the newcondition-join.target-table-resolved-at-parseload-bearing-classifier key. TheWithTargetcapability splits into a smallerHasTargetTable(target read only) plus the slot-iterationWithTarget extends HasTargetTable, soConditionJoinjoinsFkJoin/LiftedHopunder one capability for the alias-declaration loop andJoinPathEmitter.targetJavaClassNamecollapse. A new sealedParentCorrelationtaxonomy (OnFkSlots/OnConditionJoin) lifts the step-0 fork between FK-slot correlation and ConditionJoin-method correlation out of every emitter site into the model, threaded through each affectedChildFieldvariant’s record header with aparentCorrelation.firstStep() == joinPath.get(0)compact-constructor invariant. The inline emitters (InlineTableFieldEmitter,InlineLookupTableFieldEmitter,InlineColumnReferenceFieldEmitter) and the split-rows emitter (SplitRowsMethodEmitter’s `buildListMethod/buildSingleMethod/buildConnectionMethod) read the dispatch off the carrier; FK hops emit.join(alias).onKey(FK), condition hops emit.join(alias).on(method(prev, this)). For split-rows +OnConditionJoin, the prelude declares a freshparentAliasTable local for the @table-bound parent, emits the step-0.join(parentAlias).on(condition(…))clause, and routesparentInputto JOIN on parent-PK columns. Validator-side:validateVariantIsImplemented’s `SplitRowsMethodEmitter.unsupportedReasonconsult andvalidateColumnReferenceField’s `hasConditionJoinbranch both delete;validateReferenceLeadsToTypefolds itsWithTargetspecial-case onto a uniformHasTargetTableread. TheConditionJoinReportablecapability interface deletes outright;Rejection.EmitBlockReasonenum (six values, all condition-join-step) andRejection.StubKey.EmitBlockrecord retire with their last producers;JoinPathEmitter.hasConditionJoinpredicate retires with its last consumer;docs/manual/reference/diagnostics-glossary.adoc’s six `=== <variant>-condition-join-stepheadings delete perDiagnosticsDocCoverageTest. R129 absorption: thecolumn-reference-on-scalar-field-condition-joinslug closes ;ColumnReferenceFieldwith a multi-hop path containing a condition step now classifies and emits viaInlineColumnReferenceFieldEmitter; a single-hop condition-only path on a scalar return type AUTHOR_ERRORs at the parser with actionable rewrite guidance (use{table:}or{key:}), which is the same diagnostic shape the deferred-rejection used to surface, now produced one stage earlier. Tests. Pipeline-tierValidationTestcases flip from deferred-rejection to no-error; newHasTargetTableInvariantTestpins the JoinStep-permits-implement-HasTargetTable invariant;ParentCorrelationFirstHopInvariantTestexercises bothOnFkSlotsandOnConditionJoinarms end-to-end;GraphitronSchemaBuilderTestgainsCONDITION_ONLY_TERMINAL_RESOLVES_TARGET_FROM_RETURN_TYPE,TABLE_WITH_CONDITION_PRESERVES_WHERE_FILTER,KEY_WITH_CONDITION_PRESERVES_WHERE_FILTER(the last two are regression guards for the legacy{table:, condition:}/{key:, condition:}whereFilter-fold semantics),CONDITION_ONLY_NO_RETURN_TYPE_TABLE_REJECTED(AUTHOR_ERROR when the terminal-hop carrier’s return type has no@table),CONDITION_INTERMEDIATE_REFLECTS_METHOD_PARAM(reflection on the condition method’s second parameter type resolves the intermediate-hoptargetTable), andCONDITION_INTERMEDIATE_TABLE_WILDCARD_REJECTED(AUTHOR_ERROR when the intermediate condition method usesTable<?>); plus extends the existingWITH_CONDITION_PATHfixture with a non-nulltargetTable()check. *Symmetric finish onRecordTableMethodField: the seventh@record-parent variant gains aParentCorrelationfield on its record header so a {condition:}-first path AUTHOR_ERRORs at parse time (same shape as the siblingRecordTableField/RecordLookupTableFieldvariants ;@record-parents have no@tableto anchor the condition method’s source arg, so the synthesis routes through AuthorError); the deadinstanceof JoinStep.FkJoinarm inSplitRowsMethodEmitter.buildForRecordTableMethod’s `unsupportedPathpredicate retires, leaving only the pre-existing R43 limits (empty + multi-hop). Compile-tier + execution-tier: Sakila gains two condition-method fixtures,Customer.addressByCondition: Address @reference(path: [{condition: …}])exercising the inline TableField emission shape andFilm.actorsByCondition: [Actor!]! @splitQuery @reference(path: [{condition: …}])exercising the split-rows emission shape via an EXISTS-over-junction predicate;ReferencePathConditionFixturesships the two condition methods. Compile-tier coverage flows throughmvn install(generated code compiles against the real jOOQ catalog); execution-tierGraphQLQueryTest.inlineTableField_conditionJoin_returnsAddressPerCustomerandsplitTableField_conditionJoin_returnsActorsPerFilmassert end-to-end SQL correctness against PostgreSQL (the inline test cross-checks against the FK-equivalentCustomer.addressnavigation; the split-rows test verifies one batched DataLoader round-trip across five films). Out of scope, follow-up filed: the legacyReferenceElement { table, key, condition }directive surface conflates join-shape with WHERE-filter and admits seven free combinations; the cleanup is filed as a separate Backlog item (path-element-surface-cleanup, R235). -
R229 (
ebfa633+b9121e0):EnumTypeGeneratornow honours@field(name:)on enum values by writing.name(<sdl>).value(<runtime>)into the generated<Name>Type.type()body, with the runtime string pre-resolved at classify time on a newno.sikt.graphitron.rewrite.model.EnumValueSpec(sdlName, runtimeValue, description, deprecationReason, source) carried asList<EnumValueSpec> valuesonGraphitronType.EnumType. Pre-R229 the emitter echoed.name(SDL).value(SDL)and the directive lookup was re-evaluated independently at the resolver site, so a federated subgraph returning the runtime form (e.g."FØDSELSNUMMER"forFODSELSNUMMER @field(name: …)) hitCan’t serialize value … Unknown value 'FØDSELSNUMMER'at graphql-java’s Coercing layer. With the directive lifted into the.value(…)slot, graphql-java owns the wire ↔ runtime translation at the boundary in both directions, which collapses the Java-sideCallSiteExtraction.TextMapLookuparm intoDirect: the sealed permit,EnumMappingResolver.enrichArgExtractions, theTypeConditionsGenerator/TypeFetcherGeneratorstatic_MAPemit paths, theArgCallEmitter/FieldBuilderswitch arms, and theEnumMappingResolverfield/constructor params onServiceDirectiveResolver/TableMethodDirectiveResolverall retire.EnumMappingResolver.buildTextEnumMapping/.validateEnumFilterread from the classified model’sList<EnumValueSpec>(lookup viactx.types) so the directive is read once at classify time and both consumers share the record component, eliminating the drift R263 reintroduced. Coverage: pipeline-tierGraphitronSchemaBuilderTest.EnumTypeCase.ENUM_WITH_FIELD_NAME_DIRECTIVEpins the classifier output (runtimeValue == "FØDSELSNUMMER"); existing PLAIN_ENUM / ENUM_WITH_DEPRECATED_VALUE cases reroute throughEnumValueSpec; new execution-tierEnumSerializationExecutionTestpins three boundary scenarios (directive value round-trips through Coercing on output; input round-trip delivers the runtime form to the resolver; simple-value identity fallback); unit-tierEnumTypeGeneratorTest.typeMethod_routesFieldNameDirectiveIntoRuntimeValuepins the generated.name(sdl).value(runtime)shape; the R53 regressionSERVICE_MUTATION_FIELD_NAME_OVERRIDE_TEXT_ENUMflips its assertion fromTextMapLookuptoDirect(same scenario, conversion has moved to the wire boundary). *Out of scope (called out, filed as R231): graphitron currently lowers text-mapped-enum fields to GraphQL typeStringat emit time, so R229’s.value()lift is invisible to clients on Sakila’stextRatingfield; emitting those fields as the enum type is a separate structural fix. Self-review follow-upb9121e0swept stale TextMapLookup /enrichArgExtractionsjavadoc citations acrossInputBeanResolver,ServiceCatalog,ServiceDirectiveResolver,TableMethodDirectiveResolver,ArgCallEmitter,BodyParam,ConditionFilter,InputColumnBinding,InputField, andMethodRef. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R230 (
8f59529): FixBodyParam.nonNullfor nested input fields under a nullable enclosing arg.walkInputFieldConditionsnow ANDs aneffectiveNonNullboolean (seeded from theInputTypeArg’s `nonNull()atprojectFilters, narrowed at eachNestingFieldrecursion) into the value passed to everyimplicitBodyParam/compositeImplicitBodyParamcallsite, so the emitter’s unguardedcondition.and(…)branch only fires when every enclosing link is statically non-null. Pre-R230 a query likesoknader(filter: HentSoknadInput): [Soknad!]withHentSoknadInput.soknadId: [ID!]!silently returned the empty set whenfilterwas omitted, because the generator emittedcondition.and(film.film_id.in(null))and jOOQ renders.in(null)as the literalfalse. Producer contract pinned by a newbody-param.nonnull-is-effective-runtime@LoadBearingClassifierCheckonwalkInputFieldConditions(single annotation covers both producer sites in its description text ; audit requires producer-key uniqueness) paired with a@DependsOnClassifierCheckonTypeConditionsGenerator.buildConditionMethod.BodyParam.nonNull’s interface-level javadoc tightens to name the producer / emitter contract; the accessor’s one-liner reduces to a forward-pointer. Coverage: `NestedInputFieldEffectiveNonNullPipelineTestpins the three AND transitions on the classified slot (nullable arg → false, both non-null → true, nullableNestingFieldwrapper between non-null arg and non-null leaf → false);GraphQLQueryTest.filmsByEffectiveNullability_omittedFilter_returnsUnfilteredBaselineis the only tier that observes jOOQ’s.in(null)rendering, asserting the omitted-filter case returns the unfiltered baseline of 5 films rather than the empty set. -
R223 (
9c25edc+c8813a4):roadmap-toolgains acheck-adoc-tablesverify-phase subcommand that walks every authored.adocundergraphitron-rewrite/anddocs/, tracks five structural block types (|===table,----listing,….literal,////comment,passthrough), and fails the build on any markdown-separator row (|---|---|, with optional GFM alignment colons) found outside all blocks. Asciidoctor renders markdown table syntax as paragraph text with literal pipes, so the typo was invisible until publish; the truth table atgraphitron-rewrite/docs/argument-resolution.adoc§ "Truth table (per input-field, per call site)" was carrying this shape and is converted to AsciiDoc[cols=…]+|===syntax in the same commit.target/,node_modules/, and.git/subtrees are skipped;.mdfiles are out of scope (markdown table syntax is native there).AdocMarkdownTableCheckTestpins six fixtures: markdown separator outside any block is flagged; the same characters inside each of the five block types are not flagged;target/directories and.mdfiles are skipped by the walker; aligned (|:---|---:|) separators are flagged.CLAUDE.md"Writing style" gains a paragraph naming the rule and the new check. Out of scope (called out, owned by R227):mdBodyToAdocdoes not translate markdown tables embedded in.mdroadmap plans, so rendered roadmap.adocundertarget/still carries raw pipe rows; that render-side hole is tracked separately. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; verify phase reports "no markdown-formatted tables in authored .adoc files". -
R228 (
de25b0a+ac5830d):GraphitronSchemaValidatornow rejects inlineTableField/LookupTableFieldwhose@referencepath carries a@conditionstep at build time, closing the gap that let those two variants reach a runtimeUnsupportedOperationExceptionwhile the four sibling variants surfaced aRejection.Deferredbuild error.ChildField.TableFieldandChildField.LookupTableFielddeclareConditionJoinReportablewith their ownEmitBlockReasonvalues (TABLE_FIELD_CONDITION_JOIN_STEP,LOOKUP_TABLE_FIELD_CONDITION_JOIN_STEP) anddisplayLabel("Inline TableField", "Inline LookupTableField");SplitRowsMethodEmitter.unsupportedReasonis the single predicate the validator and both inline emitters consult, so inline stubs render byte-for-byte the same message as before and the fourfour ChildField variantsjavadoc/comment sites widen to six. Tests:R58TypedRejectionPipelineTestgainsinlineTableField_conditionJoinStep_rejectedAtBuildTime+inlineLookupTableField_conditionJoinStep_rejectedAtBuildTimepipeline-tier coverage; the existing seal-tracking assertion renamesconditionJoinReportable_implementedByExpectedFourVariants→conditionJoinReportable_implementedByExpectedSixVariants;TableFieldValidationTest/LookupTableFieldValidationTestflipWITH_CONDITION_ONLYfrom "no error" to "stub surfaces as build error" and addLIST_WITH_CONDITION_ONLY.docs/manual/reference/diagnostics-glossary.adocadds=== table-field-condition-join-stepand=== lookup-table-field-condition-join-stepparagraphs alongside the four existing entries (DiagnosticsDocCoverageTestgate). Sakila example:Category.similarwas an inline-TableFieldConditionJoin fixture deliberately admitted by the classifier and stubbed at runtime; the validator now rejects it at build time (intended outcome), so the field and its sole dependentCategoryConditionsclass are removed from the example along with theREADME.adocreference. Out of scope (called out, owned by R3 item 5 + R129): lifting the condition-join restriction itself ; when item 5 ships all six variants' validator arms come out together with the two inline emitter stubs and the four runtime stubs inSplitRowsMethodEmitter. Note on test placement: spec namedGraphitronSchemaBuilderTestbutGraphitronSchemacarries norejections()accessor; fixtures landed inR58TypedRejectionPipelineTest(existing home for "build schema + run validator + assert typed rejection"). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R224 (
5b202fb+d1e8fd6): LSPDiagnostics.validateFieldMemberconsults the field classification before falling back to the type-backing table, so@field(name:)on a@reference(path:)field validates the column against the path’s terminal table instead of the enclosing type’s@table. ForColumnReference/CompositeColumnReferencearms the lookup now resolves throughFieldClassification.tableName()(projected viaCatalogBuilder.terminalTableName), mirroring the runtime’sServiceCatalog.resolveColumnForReferencewalk;Column/CompositeColumnarms route the same way (equivalent target table, sourced from the classification);InputUnbound/Unclassifiedarms stay silent because the validator already emits a precise message; other arms fall through to the existing backing-driven dispatch. The fix also addsTypeContext.enclosingFieldOrInputValueDefinitionso the dispatch resolves the SDL field name on input-sideinput_value_definitionnodes too (the priorenclosingFieldDefinitionwalked only output-sidefield_definition). The new emitter site wears@DependsOnClassifierCheck(key = "field-classification-payload-faithful")against the existingCatalogBuilderproducer. Tests:DiagnosticsTestgains three regression cases driving syntheticLspSchemaSnapshot.Built.Currentsnapshots: input@table+@referenceretargets to terminal-table column, output@table+@referencemirror, and silence-on-Unclassified(no duplicate "Unknown column … on table '<enclosing>'" diagnostic). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R225 (
9b753db): LSPDiagnostics.severityOfflipsRejection.DeferredfromDiagnosticSeverity.WarningtoError, so the editor squiggle matches themvn graphitron:devfinality (everyRejectionvariant throwsValidationFailedException, regardless of arm ; the actionable hint is the roadmap-item slug carried by the rejection, not the severity, reverting the R147 softening).ValidatorDiagnosticsTest.deferredMapsToWarningSeverityrenamed todeferredMapsToErrorSeveritywith its severity assertion flipped;RejectionSeverityCoverageTestunchanged (asserts only non-null). Build green: fullgraphitron-lsptest suite (347 tests) passes on Java 25. -
R216 (
2a15e15+70e41cb): LSP classification, hover, inferred-directive, completion, go-to-definition, and@field(name:)member-validation surfaces now walkextend type X { … }declarations in parallel withtype X { … }definitions. A new closed-familyDeclarationKindenum (graphitron-lsp/…/parsing/DeclarationKind.java, 12 constants spanning both_type_definitionand*_type_extensionkinds) replaces the three out-of-syncSet<String>sources of truth inInlayHints,TypeContext, andDeclarationHovers;DeclarationKind.enclosing(Node)+DeclarationKind.walkAll(Node, Consumer)centralise the two walks every consumer used;isCarrier()filters the field-hover ancestor walk to coordinates whereParent.fieldNameis meaningful.TypeContext.tableNameOfis rerouted through the classifier’s name-keyed projection on the snapshot (built.typeClassificationsByName().get(name)→tableNameFromClassification) so anextend type Customer { … }whose@table-bearing definition lives in another file still resolves to the authoritative table name; the privatetableNameOf(TypeClassification)helper inInlayHintslifts toTypeContext.tableNameFromClassificationso inlay / hover / completion / definition / diagnostic surfaces share one switch. Snapshot threading reachesReferenceCompletions.generateandDefinitions.compute(both gain@DependsOnClassifierCheck(key = "type-classification-payload-faithful")so the audit test pins their new dependence on the type-classification projection);GraphitronTextDocumentServicewiresworkspace.snapshot()to both. Tests:InlayHintsTestgains classification, inferred-@field, and absent-@tableparity onextend type Query+extend type Customer;DeclarationHoversTestgains type-name and field-name hover parity insideextend type Customer;DiagnosticsTestgains@field(name:)member validation insideextend type Foo(unknown column + valid column); the existingReferenceCompletionsTest.unknownTableReturnsEmptyForKeyadapts to the snapshot-as-source-of-truth posture (classifier mapsFooto a missing table, completion empties). Self-review fix (70e41cb) dropped a defensiveLinkedHashSet<Node>dedupe fromwalkAll; tree-sitter ASTs are trees, the set guarded against an impossible scenario. *Out of scope (called out): generator-side admission ofextend type Foo @table(name:"x") { … }(the classifier doesn’t see@tableon extensions today, so the snapshot-routedtableNameOfstays silent on extension-declared@tablewithout a corresponding definition ; lifting that constraint is a classifier-side change with its own roadmap item). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; 1905 graphitron + 337 graphitron-lsp + sakila-example tests pass. -
R217 (
ee2802f+72fd454): LSP inlay classification labels surface model leaf names, and a synthetic@table(name: "…")ghost now renders on declarations whose classification isTable/Node/TableInterface/TableInputbut that carry no@tabledirective at all.LspClassificationLabels.projectionLabel/projectionTypeLabelreturn each projection record’ssimpleName()verbatim ("Column", "Table", "DmlMutation", …) via exhaustive uniform-body switches that survive as compile-time tripwires for new permits;DeclarationHoversprints the qualified form (FieldClassification.Column/TypeClassification.Table) in hover headers;FieldClassification/TypeClassificationclass-level Javadoc records the new dual role of projection-record names. The generator-permitfieldLabel(GraphitronField)/typeLabel(GraphitronType)variants had no LSP callers and are deleted.InferredDirectiveArgs.Entrygains anAbsentArm absentArmslot (initial implementation used aboolean renderWhenAbsentflag, replaced in the self-review fix by a sealedAbsentArmstrategy interface so a future entry that wants absent-rendering must implement or reuse a permit: flipping the field on without a matching renderer no-ops at compile time, not at runtime, preserving the canonical-arg table’s invariant "downstream consumers either pick it up automatically or fail to compile"). Today only the@tableentry carries an arm (AbsentArm.TableName) whose switch encodes the eligibility set and readstableName()offTypeClassification.{Table, Node, TableInterface, TableInput};@field/@referencestay off per the spec’s judgement calls.InlayHints.collectAbsentDirectiveHintswalks type-definition nodes in parallel with the classification arm, dispatching toentry.absentArm().resolveAbsentValue(c)via virtual call, emitting@<directive>(<arg>: "<resolved>")anchored at the type-name node when the type carries no directive of that name; the existing present-but-bare arm and the new absent arm share the canonical-arg table and theconfig.inferredDirectives()toggle. The@DependsOnClassifierCheck(key="type-classification-payload-faithful")reliesOntext widens to note the absent-directive arm. Tests:InlayHintsTest.classificationHintsLabelFieldDeclarationsflips from"table type", "column"to"Table", "Column"; newabsentTableHintRendersOnObjectTypeWithoutDirective,absentTableHintRendersOnInputTypeWithoutDirective, andabsentTableHintSuppressedWhenDirectivePresentpin the new arm on object + input declarations and assert it stays quiet when the directive node is present;inferredTableHintSuppressedWhenAuthoredextends tononeMatch(label → label.startsWith("@table"));DeclarationHoversTestflips header assertions to the qualified form. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; 337 graphitron-lsp tests + 1897 catalog tests pass. -
R215 (
fdb757b+3d60f40): column-binding requirement captured at classification, not derived at usage.BuildContext.classifyInputFieldgains aClassifyContextparameter (carriesexpandingTypes+enclosingOverride); the recursive descent throughNestingFieldcomposesctx.expanding(typeName).withOverride(ctx.enclosingOverride() || nestOverride).InputField.ConditionOnlyFieldrenamed toInputField.UnboundField(parentTypeName, name, location, typeName, nonNull, list, Optional<ArgConditionRef> condition, String attemptedColumnName); the classifier emits this variant uniformly on column-miss (across plain and@tableinputs) and on@condition(override: true)with a matching column (the §5ColumnField+override:truecollapse).TypeBuilder.buildTableInputTypedefers column-coverage to consumption (admitsUnboundFieldinstead of rejecting the whole type asUnclassifiedType);FieldBuilder.walkInputFieldConditionsbecomes a single exhaustive switch with theUnboundFieldarm consumingenclosingOverridedirectly and emitting a consumer-side rejection (typedRejection.AuthorError.UnknownNamewith Levenshtein hint) when the cascade doesn’t admit.GraphitronSchemaValidatorwalksTableInputType.inputFields()and rejectsUnboundField + @condition(override: false)at the directive’s source location;MutationInputResolver.resolveInputadmitsUnboundField(condition: present, override: true)on UPDATE / DELETE and rejects on INSERT, plus rejects@condition(override: false)on any mutation input field at SDL-walk time. Eight downstream sealed-switch consumers updated (walkInputFieldConditions,MutationInputResolver,EnumMappingResolver,CatalogBuilder,ContextArgumentClassifier,GraphitronSchemaValidator,TypeFetcherGenerator.NOT_DISPATCHED_LEAVES, plus LSPLspClassificationLabels+DeclarationHoversandFieldClassification.InputUnboundrenamed fromInputCondition);InputFieldResolver.resolve(typeName, rt, enclosingOverride)takes the cascade flag fromFieldBuilder.classifyArgument(fieldOverride || argCondition.map(c → c.override()).orElse(false)). Two new load-bearing classifier-check keys:input-field.unbound-implies-no-column(producer:classifyInputFieldInternal; consumer:walkInputFieldConditions) andinput-field.unbound-with-override-condition-admits-on-mutation-update-delete(producer:resolveInput). Coverage: eight new R215 acceptance tests inGraphitronSchemaBuilderTest(r215_plainInputArgLevelOverrideAdmitsNonBindingField,r215_tableInputNonBindingFieldRejectsAtConsumer,r215_tableInputNonBindingFieldAdmittedUnderOverrideCascade,r215_validatorRejectsOverrideFalseOnNonBindingField,r215_validatorRejectsConditionOverrideFalseOnMutationInputField,r215_mutationUpdateConditionOverrideTrueOnNonPkFieldAdmits,r215_mutationInsertConditionOverrideTrueRejects,r215_nestedPlainInputPropagatesCascade); R210’s renamedplainInput_overrideTrueWithoutMatchingColumn_classifiesAsUnboundField+tableInput_overrideTrueWithoutMatchingColumn_classifiesAsUnboundFieldstay green; six existing tests asserting the pre-R215 rejection shape (EXPLICIT_TABLE_UNRESOLVED_COLUMN,NESTED_INPUT_FIELD_UNKNOWN_COLUMN,NodeIdPipelineTest.InputCase.{ACCESSOR_MISSING, LIST_VARIANT}) updated to assert the new admit-at-type-build behaviour. Late-round patch (after self-review withprinciples-architect, alf’s pushback on cascade-contract gloss): the first-passwalkInputFieldConditionsUnboundField arm silently dropped the inner@conditionunder an outer@condition(override:true)cascade, contradictingdocs/manual/how-to/migrating-from-legacy.adoc#behavior-divergence-condition-cascade("every@conditionyou write produces SQL; the override flag controls only the implicit column predicate"). The arm now mirrors theColumnFieldarm structure (always emit the explicit@conditionwhen present; decide rejection separately): rejects at the consumer outside the cascade forcondition.isEmpty()(no filter contribution) andcondition.isPresent() && !override()(structurally malformed shape) ; the second arm acts as a safety net for plain inputs until R221 lifts validator coverage there. New acceptance test #11r215_innerExplicitConditionFiresOnUnboundFieldUnderOverrideCascadepins the cascade-doc contract (twoConditionFilter`s emitted: outer arg-level + inner field-level); the three R205 Path B regression tests (`ArgumentParsingCase.PLAIN_INPUT_ARG_FIELD_CONDITION_EMITTED,plainInput_unresolvedFieldWithCondition_rejectsAsUnclassifiedFieldWithUnknownName,plainInput_overrideFalseWithoutMatchingColumn_stillRejectsAsUnclassifiedField) keep their pre-R215 rejection assertions green via the consumer-arm safety net. Out of scope (called out, deferred to follow-ups):MutationField.{Value, Condition}sealed projection fromMutationInputResolverfor downstream DML emitters (acceptance behaviour shipped at the resolver’s per-field admission loop; the structural lift to a sealed projection is a follow-up roadmap item ; no emitter consumes aMutationFieldprojection yet); R213 exact-SourceLocationattribution on the surroundingUnclassifiedField’s `locationfield (the rejection prose names the field but the wrapper’s location still points at the consuming query field; threading the location throughwalkInputFieldConditions→projectFilters→projectForFilter→TableFieldComponents.Rejectedis the R213 follow-up); R221 validator walksPlainInputArg.fields()for the sameUnboundField + @condition(override:false)rejection the shipped validator catches onTableInputType(the consumer-arm safety net above covers non-cascade plain inputs; the cascade case admits-and-emits today, which R221 will reject at the directive’s location). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; 1906 graphitron tests + 334 graphitron-lsp tests + all sakila-example tests pass. -
R211 (
831a32d):@condition(override: true)build failure no longer surfaces the misleading "no column 'X' found in table 'Y'" line alongside the actionable condition error.BuildContext.classifyInputFieldInternalreinstates anerrorsBeforesize-delta check inside the R210 override:true block: whenbuildInputFieldConditionappends toerrorsand returns empty, the gate returns a placeholderInputFieldResolution.Unresolved(lookupColumn=null, "@condition(override: true) failed to build; see condition error")instead of falling through to the column-miss arm; the column is unused by construction under override:true. The override:false leg never enters this branch, so R205 acceptance test #6’s typedAuthorError.UnknownNamelift survives (InputFieldResolver.resolve’s `canLiftToUnknownNameguard seescondErrorsnon-empty +lookupColumnnull and folds toRejection.structural, which is the right bucket ; the failure shape is condition-method binding, not unknown-column). R210’s existing testplainInput_overrideTrueWithBrokenCondition_rejectsAsUnclassifiedFieldgains adoesNotContain("no column 'sakskode' found")assertion so a regression that reintroduces the column-miss arm under override:true trips at the existing test site. Surfaced by alf’s productionopptak-subgraph(parameter-name mismatches inOpptakFilterInput.opptaksNavn/utdanningstilbud); the related attribution issue from the same investigation is filed as R213. Will be subsumed by R215’s column-binding-at-classification restructure (the override:true gate moves above the column lookup, making the column-miss arm structurally unreachable on this branch); thedoesNotContainassertion stays as a regression guard. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R203 (
2b8b465+8d39ef4+9ac3e4cPhase 1 natives module + release workflow; first releaseno.sikt:graphitron-tree-sitter-natives:0.26.0-1published to Maven Central via thetree-sitter-natives-releaseworkflow on its four-platform matrix [Phase 2];ae486d9Phase 3 graphitron-lsp cutover;bbabb02BundledLibraryLookupprobe for well-knownlibtree-sitterinstall prefixes;a94f546In Review → Ready rework after first review pass;a3338b6Phase 4 distinguishes too-oldlibtree-sitterfrom missing and ships the spec-named error-translation unit test): graphitron-lsp no longer compiles a per-platform tree-sitter runtime + grammar on everymvn install. The vendoredlibtree-sitterruntime sources and the bkegleytree-sitter-graphqlgrammarparser.care gone fromgraphitron-lsp/src/main/native/(≈30 000 lines deleted); the threebuild-native-*Maven profiles and theexec-maven-pluginshell-out are gone fromgraphitron-lsp/pom.xml. The grammar binary now ships from a new same-repo standalone Maven modulegraphitron-rewrite/graphitron-tree-sitter-natives/(groupIdno.sikt, version stream<tree-sitter-runtime-ABI>-<build-n>, first release0.26.0-1; standalone pom intentionally not a child ofgraphitron-rewrite-parentand not in the parent reactor’s<modules>list, so the release cadence decouples from the rewrite’s10-SNAPSHOTparent andmvn install -f graphitron-rewrite/pom.xml -Plocal-dbpays zero build cost). The natives release workflow isworkflow_dispatch-only on a four-platform GitHub Actions matrix (linux-x86_64,linux-aarch64,macos-aarch64,windows-x86_64;macos-x86_64dropped during Phase 1 dry-run since Sikt LSP developers all run M1+); each matrix runner runs upstream’stree-sitter buildCLI against the vendored grammar to produce one platform-shaped shared library, the jar carries exactly fourlib/<os>-<arch>/tree-sitter-graphql.{so,dylib,dll}entries (POSIXlibprefix; Windows unprefixed per platform convention), and a post-deploy load+parse matrix verifies the published artifact resolves into a fresh local m2 and the bundled grammar loads against an OS-installedlibtree-sitteron every platform. graphitron-lspBundledLibraryLookupswitches to the four-platform set, drops the previousUnsupportedOperationExceptionWindows branch, and now also probes well-knownlibtree-sitterinstall prefixes (Homebrew/opt/homebrew/lib+/usr/local/libon macOS, vcpkg’s<VCPKG_ROOT|VCPKG_INSTALLATION_ROOT>/installed/x64-windows/bin+ the defaultC:\vcpkg...on Windows,/usr/local/libon Linux), composing a system-installed runtime onto the SPI grammar lookup viaSymbolLookup.orso vanillabrew install tree-sitter/vcpkg install tree-sitter:x64-windowswork with no env-var wiring.GraphqlLanguage.loadOrExplaintranslatesUnsatisfiedLinkError/RuntimeExceptionfromLanguage.loadinto an install-instructions message and now also distinguishes "too-oldlibtree-sitterinstalled" (commonly Debian/Ubuntu apt’slibtree-sitter00.20.x, which predates thets_language_abi_versionsymbol jtreesitter 0.26 looks up) via a probe-path classifier that walks aBundledLibraryLookup-superset including apt’s/usr/lib/<arch>-linux-gnu/libtree-sitter.so.0and checks the ABI symbol directly.GraphqlLanguageErrorTranslationTestpins the classifier (cause-chain walk, ABI-symbol failure shape, ignores unrelated errors), the missing- and too-old- runtime messages per OS via@EnabledOnOs, and explicitly nameslibtree-sitter0in the too-old Linux hint.NativeLibraryBundleTestcovers all four platforms via per-platform@EnabledOnOsmethods (3 skipped on any single host);rewrite-build.ymlsource-buildslibtree-sitter v0.26.9so thelinux-x86_64method runs green in CI.getting-started.adocgains a "Native runtime dependency" section with[#native-runtime-dependency]anchor and a per-platform install + library-discovery table (including a NixOS shell.nix snippet for nix-store layouts and theJAVA_TOOL_OPTIONS=-Djava.library.path=…escape hatch for non-default installs). Subsumes and deletes R89 (lsp-native-build-multiplatform-ci): the multi-platform-CI concern is now the post-deploy matrix on the natives release workflow plus the four@EnabledOnOsNativeLibraryBundleTestmethods, not a per-PR matrix onrewrite-build.yml. 334 graphitron-lsp tests pass (7 skipped: 3 platform-gatedNativeLibraryBundleTest+ 4 platform-gatedGraphqlLanguageErrorTranslationTestmethods on the Linux CI host); fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbgreen on Java 25. -
R210 (
94bc3bf+47f5f39+ac2588c):@condition(override: true)on an input field with no matching column no longer rejects asUnresolvedunder R205’s Path B. NewInputField.ConditionOnlyFieldsealed permit carries theArgConditionRefwith no column data;BuildContext.classifyInputFieldInternalgates on the directive’soverrideflag at the "no column found" fall-through (cheap read, noerrors-list side effects) before building the condition, so the typedAuthorError.UnknownNamelift onoverride:falsereflection failures is preserved at the R205 boundary.FieldBuilder.walkInputFieldConditionsemits the explicitConditionFilteronly; six other exhaustiveInputFieldconsumer sites (ContextArgumentClassifier,GraphitronSchemaValidator,EnumMappingResolver,CatalogBuilder+ newFieldClassification.InputConditionrecord,TypeFetcherGenerator.NOT_DISPATCHED_LEAVES, LSP hover + inlay) grow explicit arms;MutationInputResolver’s existing default-arm rejects condition-only carriers as structurally unfit for DML. Symmetric across plain inputs (the reported opptak-subgraph `SakFilterV2Input.sakskodeshape) and@tableinputs since both shareclassifyInputFieldInternal. Coverage: three R210 acceptance tests (plainInput_overrideTrueWithoutMatchingColumn_classifiesAsConditionOnlyFieldwith@ProjectionFor(ConditionOnlyField.class),tableInput_overrideTrueWithoutMatchingColumn_classifiesAsConditionOnlyField,plainInput_overrideTrueWithBrokenCondition_rejectsAsUnclassifiedField) plus a boundary test (plainInput_overrideFalseWithoutMatchingColumn_stillRejectsAsUnclassifiedField) pinning the R205↔R210 behaviour boundary by name;TestConditionStubgainssakskodeCondition/syntheticNameConditionfixtures;VariantCoverageTest.NO_CASE_REQUIREDcarries the rationale for the @Test-not-enum-case shape. Out of scope (called out): execution-tier Sakila fixture mirroring the production shape ; deferred (pipeline tier already exercisesclassifyInputFieldInternaland the projectedConditionFilter). Design alternative considered:InputFieldResolution.ConditionOnlyarm (resolution-tier sibling, structurally honest, deferred to a future refactor if the carrier-vs-resolution distinction becomes load-bearing); the carrier-tierInputField.ConditionOnlyFieldpermit chosen for incremental change cost + uniform walking. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; 1893 tests pass. -
R205 (
fdada52restore plain-input filter symmetry + escalate Unresolved to build error,c1de3f8self-review follow-ups + R209 backlog stub): plain-input (non-@table) filter input types now classify and project identically to@tableinputs.InputFieldResolver.resolvereturns sealedResolution.{Ok, Rejected}(mirroringOrderByResolver.Resolved); anyInputFieldResolution.Unresolvedor@conditionreflection failure lifts as a typedRejection(single column-miss →Rejection.unknownColumnso LSP fix-its consume the structuredattempt + candidates; everything else folds toRejection.structuralwith joined prose).ArgumentRef.UnclassifiedArg’s `String reasonbecomesRejection rejectionwith a backwards-compatiblereason()accessor; the four other construction sites inFieldBuilder.classifyArgumentwrap their prose withRejection.structural(…).FieldBuilder.projectFilters’ `PlainInputArgbranch is now structurally identical toTableInputArg: it allocates a non-nullimplicitBodyParamsand drains it intobodyParams, so the symmetric implicit-predicate emission is a type-system fact (the four per-callimplicitBodyParams != nullguards inwalkInputFieldConditionscollapse; the method asserts non-null at entry viarequireNonNull).projectFilters/projectForFilterthreadList<Rejection>end-to-end via a newfoldRejectionshelper;UnclassifiedArg.rejection.prefixedWith(…)preserves typed payloads (e.g.AuthorError.UnknownNamefrom a plain-input column miss) through toUnclassifiedField.rejection.ProjectionCoverageTestdrops thePojoInputTypeallowlist entry; a new@ProjectionFor(PojoInputType.class)projection test (plainInput_resolvedColumnWithoutCondition_emitsImplicitBodyParam) pins the implicit-predicate emission on the plain-input path. Six acceptance tests cover symmetric implicit emission, explicit+implicit composition, override propagation, rejection on Unresolved with/without@condition, and rejection on@conditionreflection failure (Path B: bare-field-without-@conditionsignals binding intent just as much as@condition-annotated does). ThelanguagesByPlainInputsakila-example fixture + execution test, which encoded the silent-drop as expected behaviour, are deleted;docs/argument-resolution.adocretires the per-field-skip rationale paragraph at:400-412and adds an R205 anchor sentence to the truth table at:262-275. Out of scope (called out): project-wide design-doc-vs-implementation conformance audit (filed as R207); auto-binding via@conditionmethod when no column resolves;FieldRegistryclassify-input trace’s typed-Rejection payload loss (filed as R209). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R206 (
3a44d47): synthesisedConnectionType/EdgeTypecarry the@asConnectioncarrier field’sSourceLocation.ConnectionPromoter.promotenow passesBuildContext.locationOf(fieldDef)into the two record constructors instead ofnull; first-write-wins on dedupe (the existinginstanceof ConnectionTypeearly-continuepreserves the first carrier’s location) andPageInfoType.location()deliberately staysnullbecause a single PageInfo serves every connection so no carrier site is the actionable one. Downstream,GraphitronSchemaBuilder.rejectCaseInsensitiveTypeCollisionsalready readsexisting.location()when demoting toUnclassifiedType, so SYNTH_VS_SYNTH / SDL_VS_SYNTH / SYNTH_EDGE_VS_SDLValidationError`s now carry an actionable position an LSP/editor can jump to. `CaseInsensitiveTypeClashCasemigratedConsumer<GraphitronSchema>→BiConsumer<GraphitronSchema, String>so each arm sees its own SDL fixture; SYNTH_VS_SYNTH pins both line and column (via newTestSchemaHelper.preludeLineCount); SDL_VS_SYNTH and SYNTH_EDGE_VS_SDL pin the synth side’s carrier column; SYNTH_PAGE_INFO_VS_SDL explicitly asserts the synth member’snulllocation locking in the design choice. No record-shape changes; pure provenance threading. -
R204 (
930739a): validate uniformenv.getSource()domain return type acrossOutputFieldproducers on an SDL type. LiftsOutputFieldas a sealed sub-interface ofGraphitronField(permits RootField, ChildField) declaringDomainReturnType domainReturnType(); new sealedDomainReturnType(Record(TableRef)|TableRecord(ClassName)|Plain(ClassName)) mirrors the producer’senv.getSource()Java domain identity without classloading at validator time. Post-classificationGraphitronSchemaBuilder.validateUniformDomainReturnTypegroupsOutputFieldentries by SDL Object return-type name and demotes every participant in a multi-arm group toUnclassifiedFieldwith a typedRejection.AuthorError.MultiProducerDomainTypeDisagreement; the validator carries@LoadBearingClassifierCheck(key = "output-fields.uniform-domain-return-type")paired with a matching@DependsOnClassifierCheckonFetcherEmitter.buildSingleRecordTableFetcherValue. The two formerly-@Disabledmixed-producer cases inSingleRecordTableFieldServiceProducerPipelineTestnow assert against the unified-path diagnostic; unit-tierDomainReturnTypeCoverageTestwalks the sealed-permit graph by reflection and pins per-arm structural equality. Per-permit narrowing (design fork from the spec draft’s broad-detection wording): table-bound service producers answerRecord(table)rather thanTableRecord(recordClass)because typedXRecordIS-A jOOQRecordand children read by name through the genericRecordinterface; only the carrier-payload case (DML@mutationRecord(table)vs@service-on-MutationTableRecord(XRecord)for the same payload SDL Object) surfaces today. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R190 (
182ec24sealedGraphitronContext+ schema-drivenGraphitron.newExecutionInput(DSLContext, …)factory:ContextArgumentClassifierwalks everyMethodRef.Param.Typedwhose source isParamSource.Context, rejects mutually-incompatible Java types percontextArgumentname as a typedRejection.AuthorError.TypeConflict, and stores oneResolvedContextArg(name, javaType, sites)per name on a newClassificationcarrier;GraphitronContextInterfaceGeneratornow emitspublic sealed interface GraphitronContextwith a nestedpublic static final class GraphitronContextImpl implements GraphitronContext(same-compilation-unit permits, no javapoet permits surface needed) carrying a publicINSTANCEfield and a private constructor,getTenantIdremoved,getDslContextdemoted to a default readingenv.getGraphQlContext().get(DSLContext.class);GraphitronFacadeGenerator.newExecutionInputcollapsed to a single overload withDSLContext defaultDslfirst then one parameter perResolvedContextArgin alphabetical order, body null-checks every slot and populatesGraphQLContextwithDSLContext.class, each contextArgument string key, and the singletonGraphitronContextImpl.INSTANCEunderGraphitronContext.class; five DataLoader name emission sites de-prefixed (DataLoaderFetcherEmitter,TypeFetcherGenerator×2,MultiTablePolymorphicEmitter×2,QueryNodeFetcherClassGenerator);HandleMethodBodyfederation entity dispatch grouping collapsesMap<Integer, Map<String, List<Object[]>>>toMap<Integer, List<Object[]>>;graphitron-sakila-examplemigrated end-to-end (deletedAppContext.java,GraphqlResourcecallsGraphitron.newExecutionInput(dsl, "test-user"), 14 anon-impl test sites collapsed, twogetTenantId-override tests commented out with forward-reference to R45);8d9948614-page user-doc rewrite (getting-started.adoc,runtime-extension-points.adoc,runtime-api.adoc,test-your-schema.adocsubstantive rewrites;tenant-scoping.adoc,apollo-federation.adoc,split-vs-inline.adocdeferral banners pointing at R45; index + in-prose touch-ups acrosshow-it-works.adoc,batching-model.adoc,06-going-further.adoc,add-custom-conditions.adoc,security.adoc,graphitron-rewrite/docs/README.adoc);b408253L2ContextArgumentTypeAgreementTest(accepted + three-site conflict fixtures) + L4ContextArgumentTypeAgreementValidationTest(pins the validator-mirrors-classifier drain renders header + indented per-site lines and exposes the typedsitesfield);f1a6b7aL4GraphitronFacadeGeneratorPipelineTest(classified two-@service(contextArguments)-site SDL; asserts alphabetical parameter ordering, per-slotrequireNonNull, thegraphQLContextlambda body’s typed/string puts, theDataLoaderRegistryattach), L5 example SDL gains the single@service(contextArguments: ["userId"])site (Query.greetingByUser→UserGreetingService.greet), L6FilmContextArgumentRoundTripTest(round-trip threading through to the service method; singleton-throws-on-missing-with-factory-hint diagnostic; hand-rolled-ExecutionInput.Builder-redacts-through-framework end-to-end);0aa1ee7self-review pass addressing principles-architect findings ;Classificationcached onGraphitronSchemaas a 6th component populated once at parse boundary (validator + facade emitter both readschema.contextArguments()rather than re-classifying, restoring the "single producer" framing the load-bearing-classifier annotations promised), deadgraphitronContextCallparameter dropped fromDataLoaderFetcherEmitter.buildplus its threeTypeFetcherGeneratorcall sites,CallParamlifted to carryTypeName javaTypesoArgCallEmitter’s two Context arms read identical structural data instead of one round-tripping through `ClassName.bestGuess, body-string assertion on the pipeline test deleted (covered by L5 compile + L6 round-trip),Class<T> expectedTypeslot dropped fromgetContextArgumentand the Java cast moved to the generated call site ((String) graphitronContext(env).getContextArgument(env, "userId")) ; the factory’s typed parameter list IS the load-bearing diagnostic and the runtimeexpectedType.castwas redundant ceremony;Classification.resolvedswitched fromMap.copyOftoCollections.unmodifiableMap(new LinkedHashMap<>(…))so the alphabeticalTreeMapiteration order survives the defensive copy across JVM hash seeds): single-tenant slice of R45 lands the sealed contract + the schema-driven factory so the multi-tenant rescope can layer tenant-column classification,byTenantoverload, per-loader name partitioning, and the@tenantIddirective on top of a stable baseline. Out of scope (called out, all reserved for R45): tenant column Mojo config, tenant-scope classification,byTenantfactory overload, DataLoader name partitioning by tenant,@tenantIdARGUMENT_DEFINITION directive. Custom validator factory (<validatorFactory>Mojo element) reserved for R192. Follow-ups flagged by self-review, non-blocking: the singleton’sINSTANCEis publicly callable (the L6 missing-value test reaches in directly to inspect the un-redacted message text; javadoc explicitly frames the throw as server-log surface only). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; 1870 graphitron tests + 347 example tests pass. -
R160 (
c2dc8d1C1-C3 sealedFieldClassification+TypeClassificationprojection families plusCatalogBuilderprojector switches landing onLspSchemaSnapshot.Built.{Current,Previous}symmetrically,d02a0bbC4-C5InlayHintsprovider with inferred-directive + classification arms plusInlayHintConfigandworkspace/didChangeConfigurationpush,039841bC6 classification hover via sealedDeclarationHoverparallel dispatch inHovers.compute,3d42eebC7docs/manual/reference/lsp-inlay-hints.adoc,f647ea5self-review pass 1 adding theworkspace/configurationinitialisation pull, theInferredDirectiveArgsindirection,MutationService.tableNameparity withQueryService, and aR160leak from the user-facing doc,366b07bself-review pass 2 co-locating projection-payload assertions insideGraphitronSchemaBuilderTest’s existing `// ===== <VariantName> =====classifier blocks via@ProjectionForplus theProjectionCoverageTestdrift-prevention meta-test): surfaces Graphitron’s inference and classification layers in the editor as inlay hints + rich hover. Three independent client-side toggles all default tofalseand live undergraphitron.inlayHints.inferredDirectives(ghost annotations at bare@table/@field/@referencesites showing the resolved value),graphitron.inlayHints.classification(compact label per field declaration and type declaration), andgraphitron.hover.classification(markdown unpacking the variant payload ; table, column, FK chain, target type, error channel, DML verb, …). Both projection families are sized to distinct hover-payload shapes rather than 1:1 with the generator-side permits, with discriminator fields collapsing siblings that differ only in a label axis (e.g. fourMutationField.DmlTableFieldpermits collapse to oneDmlMutation(tableName, inputTypeName, errorChannelName, DmlKind)record); the projector’s exhaustive switch over the generator-side permits is the load-bearing coverage contract that fails-to-compile on a new leaf without an LSP-side projection arm. Inferred-directive provenance is read from the live tree-sitterTreeonWorkspaceFile.tree()at request time (the AST asks "did the buffer carryname:?") rather than lifted onto the model ;Provenancediscriminators were attempted under a previous design and rolled back (~330 lines acrossTableRef,ColumnRef, the five@reference-permits,ParticipantRef.CrossTableField); the AST-read keeps the parse boundary closed and adds no model surface.BuildArtifactsshape unchanged: the newfieldClassificationsByCoord/typeClassificationsByNamefields live insideLspSchemaSnapshot.Built.{Current,Previous}symmetrically,Workspace.demoteSnapshotpreserves them, and stale-snapshot rendering mirrorsuserArgHover/columnHover’s "prefer stale info over silence" policy. C6 introduces a sealed `DeclarationHoverfamily (FieldDeclarationHover/TypeDeclarationHover) parallel to the directive-arg-keyedBehaviorfamily rather than wideningBehavior(the "Capability vs. sealed-switch confusion" principle:Behaviorstays directive-argument-binding-shaped, SDL declaration coordinates get their own resolver). Two new@LoadBearingClassifierCheckkeys (field-classification-payload-faithful,type-classification-payload-faithful) wear onCatalogBuilder.buildSnapshotwith matching@DependsOnClassifierCheckannotations on the three LSP consumers (inferred-directive arm, classification arm, classification hover). Tests: pipeline-tierGraphitronSchemaBuilderTestblocks gain@ProjectionFor-annotated sibling assertions running each canonical fixture through the projector and pinning the projected record type + payload values (24 new@Testmethods over ~20 variant blocks);ProjectionCoverageTestwalksGraphitronField/GraphitronTypesealed leaves and fails on any leaf without a@ProjectionForcover or a documentedNO_PROJECTION_REQUIREDexception; LSP-tierInlayHintsTestcovers config gating, the three inferred-directive arms, the classification arm, andBuilt.Previousstale rendering;DeclarationHoversTestcovers field-name and type-name cursor positions, theDmlMutationpayload shape, the directive-arg cursor short-circuit, andUnavailable/ missing-projection no-ops;GraphitronLanguageServerTestcovers the initialisation-timeworkspace/configurationpull;GraphitronWorkspaceServiceTestcovers the push-sidedidChangeConfigurationparse. Out of scope (called out, not regressed): inlay hints for inferred arguments on directives other than@table/@field/@reference(@nodeId(typeName:),@reference(key:)inference, and future cases extendInferredDirectiveArgs.ENTRIESand gain a renderer arm); inferred return shapes on root fetchers and the inferred join key on@nestingField; a graphitron-shipped editor extension (the LSP exposes the config keys; the editor flips them); inlay hints reflecting unsaved buffer state without a successful generator pass (hints derive from the snapshot,Unavailablemeans no hints). Follow-ups flagged by review, non-blocking:LspClassificationLabels.{fieldLabel,typeLabel}(model permit)switches are unused at runtime (projection-keyed callers cover both consumers) and can be deleted in a cleanup pass;InferredDirectiveArgslives in thecatalogpackage with its own string literals rather than underBuildContext’s `ARG_NAME/ARG_PATH, so a rename or new inference rule still maintains two places ; consolidate when a third consumer surfaces. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; 1869 graphitron tests + 318 graphitron-lsp tests pass. -
R191 (
2e84effinitial-import omnibus carrying spec + implementation + tests): honor@field(name:)for accessor lookup on free-form@recordparents on the table-bound and polymorphic-hub branches.FieldBuilder.collectAccessorMatchestakes anaccessorBaseNameparameter and matches against<base>/get<UcBase>/is<UcBase>instead of the SDL field name;deriveAccessorRecordParentSource,resolveRecordParentSource, andderivePolymorphicHubSourcethread the value from their callers (fieldNameretained for cardinality-mismatch text that quotes the SDL name). TheTableBoundReturnTypearm atFieldBuilder.java:3700reuses the already-computedcolumnName;classifyRecordParentPolymorphicChildreads@field(name:)at:4327-4329before dispatching toresolvePolymorphicRecordParent. Theaccessor-rowkey-shape-resolvedandaccessor-rowkey-shape-resolved-against-hub@LoadBearingClassifierCheckdescription blocks each gain a sentence: matched accessor’s name is the directive value when present on a free-form@recordparent, else the GraphQL field name. TheAccessorRef.methodName()value remains the actual reflected method name, so emitters (buildAccessorKeySingle/buildAccessorKeyMany,TypeFetcherGenerator.buildRecordBasedDataFetcher) invoke by name without caring how it was selected. Restores symmetry with the scalar/result branch on the same parent shape (resolveRecordAccessoralready threaded the directive value asaccessorBaseName). Coverage: pipeline-tierGraphitronSchemaBuilderTest.AccessorDerivedSourceCaseaddsACCESSOR_ROWKEYED_FIELD_NAME_REMAPS_ACCESSOR(admit onRemappedPayloadwith@field(name: "filmRecord"), assertsRecordTableField+AccessorCall.accessor().methodName() == "filmRecord"+ cardinalityONE) andACCESSOR_ROWKEYED_FIELD_NAME_REJECTS_WITHOUT_DIRECTIVE(pin the divergent-accessor-no-directive arm still falls through to the three-option AUTHOR_ERROR);RecordParentMultiTablePolymorphicPipelineTest.childInterfaceField_recordParent_accessorKeyedMany_fieldNameRemapsAccessorcovers the polymorphic-hub admit (ListPayloadparent, SDL fieldreferrerswith@field(name: "films"), asserts hubfilm+AccessorCall.methodName == "films"+ cardinalityMANY). NewAccessorPayloads.RemappedPayload(FilmRecord filmRecord)fixture record. Out of scope (called out): renaming or restructuring@field(name:), the FK-derivation path (catalog-metadata-driven, structurally indifferent to the directive), and the three-option rejection text. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-db -P!docson Java 25. -
R194 (
5e5f5e3builder pass + 5 pipeline cases,a1feaceself-review:EmitsPerTypeFilecapability + typedCaseFoldCollision+ 2 cases,f69b479preserveCaseFoldCollisionunderprefixedWith): rejects case-insensitive type-name collisions at build time.GraphitronSchemaBuilder.rejectCaseInsensitiveTypeCollisionsruns post-ConnectionPromoter.rebuildAssembledForConnections(rather than post-promote, so the assembledGraphQLSchematypeRefs stay resolvable when a synth Connection is demoted) and case-folds viaLocale.ROOT; every member of each case-equivalent group demotes toUnclassifiedTypecarrying a typedRejection.InvalidSchema.CaseFoldCollision(group, origin, prefix)withOrigin∈{SDL, SYNTH_CONNECTION, SYNTH_EDGE, SYNTH_PAGE_INFO}.message()specialises the actionable fix hint per origin (@asConnection(connectionName:)for synth arms, generic rename for SDL);validateUnclassifiedTypeprojects oneValidationErrorper member. The emit-vs-no-emit split is lifted out ofGraphitronTypeonto a newEmitsPerTypeFilecapability marker (mirrorsSqlGeneratingField/BatchKeyField), implemented by every variant exceptScalarTypeandUnclassifiedType; detector filters viainstanceof EmitsPerTypeFile.prefixedWithreturns a same-variantCaseFoldCollisionwith accumulated prefix rather than degrading toStructural, satisfying R58’s typed-rejection-preserved-under-wrap contract (the validator’sprefixedWith("Type 'X': ")is the only path that reachesValidationError.rejection). Coverage:GraphitronSchemaBuilderTest.CaseInsensitiveTypeClashCaseparameterised over 7 SDL fixtures (SDL_VS_SDL,SYNTH_VS_SYNTH,SDL_VS_SYNTH,SYNTH_EDGE_VS_SDL,SYNTH_PAGE_INFO_VS_SDL,THREE_WAY_GROUP,NO_CLASH_BASELINE);RejectionRenderingTest.prefixedWithPreservesCaseFoldCollisionTypedFieldspins single + re-prefixed paths;RejectionSeverityCoverageTest+SealedHierarchyDocCoverageTestpick up the new permit;typed-rejection.adoccarries the prose + mermaid-class entry. Out of scope (called out): legacyMakeConnections/graphitron-schema-transformclassifier, auto-mangling colliding names, federation cross-subgraph clashes, derived-filename collisions beyond the type-name stem. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R196 (
c42ed64route Workspace recalc through listener seam): lifts "drain follows enqueue" from author-discipline at three of sixWorkspacepublic mutators (didOpen,didChange,didClosepaired with explicitpublishDiagnosticsForRecalculate();setBuildOutput,demoteSnapshot,markAllForRecalculationunpaired and reachable fromDevMojo.regenerate/rebuildCatalogafter schema-file + classpath watcher events) to a structural invariant across all six. New privateenqueueAndNotify(Runnable)helper performs the queue mutation underlockand fires a single-slotvolatile Runnable recalculateListenerafter lock release; the six public mutators route through it (setBuildOutput/demoteSnapshottransitively viamarkAllForRecalculation), sotoRecalculatewrites only happen inside the funnel.GraphitronTextDocumentService.setClientregistersthis::publishDiagnosticsForRecalculateas the listener; the explicit publish calls indidOpen/didChange/didCloseare gone, and the build-trigger paths now publish diagnostics on save without waiting for the next keystroke.didClose’s "clear-for-closed-file" one-shot stays, repositioned before the workspace call to keep the seam uniform (the only direct client call in the LSP service is the close-clear; everything else flows through the listener). Lock-release-before-listener-fire is deliberate: it keeps a build swap on the watcher thread and an editor event on the lsp4j thread from serialising on `lockthrough the heavyDiagnostics.computebody; idempotency on the drain side (a seconddrainRecalculateafter the first empties the queue returns an empty list) makes "listener fires twice for two mutations interleaved with one drain" a no-op rather than a hazard. Tests: unit-tierWorkspaceTest.everyPublicQueueMutatingMethodFiresTheListenerparametrises over the six mutators asserting listener-fire count delta of exactly 1;recalculateListenerDefaultsToNoOpForTestHarnessespins that mutators on a workspace withoutsetRecalculateListenerdo not NPE;drainRecalculateIsIdempotentOnEmptyQueuepins the single-extraction property the listener path depends on;demoteSnapshotOnNoOpDoesNotFireListenerparametrises over the two no-op starting states (Unavailable,Built.Previous) pinning the only public-mutator path that returns without firing the listener ; the exception branch of the otherwise-uniform "every public mutator notifies" rule. Pipeline-tierBuildTriggerPublishesDiagnosticsTestcapturespublishDiagnosticscalls on a stubLanguageClientand drives the three-step sequence (didOpen empty → setBuildOutput with validator error → setBuildOutput with empty report); pre-R196 the second assertion failed because the listener didn’t fire. Retires the LSP-side half of R149’s deferred end-to-end publish-diagnostics wire test (the producer-sidebuildOutput()report-population test stays under R149). Out of scope (called out, not regressed): multi-consumer fan-out (one consumer today; lift when a second appears); richer event shapes (sealedRecalculateEventdiscriminating editor / build / demotion causes ; drain is cause-agnostic, sub-taxonomy carries no information consumers act on differently); non-DevMojocallers ofmarkAllForRecalculation/demoteSnapshot/setBuildOutput. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R197 (
a755e39wire LSP didSave to in-process regen trigger,30473f9In Progress → In Review): wiresGraphitronTextDocumentService.didSaveto aConsumer<String> onSchemaSavedlistener constructor-injected viaGraphitronLanguageServerand propagated throughDevServerfromDevMojo.DevMojo.buildSaveListener(suffixes, debounce, regen)(package-private static) filters URIs byRewriteContext.schemaFileExtensions()and schedulesregenerate(workspace)through the sameschemaDebouncethe FS watcher uses, so editor saves and watcher events coalesce on a single regen.DebounceExecutorconstruction hoisted fromstartSchemaWatcherup intoexecute()so the listener can be built beforebindServer. Headless LSP-only use sites (standaloneLauncher, existingTextDocumentServiceTestfixtures) keep their behaviour via no-arg / one-arg constructor defaults that pass a no-opConsumer.Workspaceis unchanged: the seam lives at the language-server boundary, not in the workspace, so extension-set ownership stays in the Mojo and the LSP module remains suffix-agnostic. Tests:TextDocumentServiceTest.didSave_invokesListenerWithUripins the URI-typed listener contract,didSave_noopWhenListenerAbsentpins the headless contract,DevMojoTest.saveListener_schemaSuffixSchedulesRegencovers the suffix filter and debounce scheduling. Docs:getting-started.adocdev-loop prose calls out the dual-path model (LSP didSave primary, FS watcher headless fallback) and the Mermaid diagram gains the LSPdidSave → dispatcharrow;DevMojoclass-level javadoc updated to match. Out of scope (called out, not regressed):didChangeregen (save is the user’s intentional commit point; mid-typing buffers are partial SDL); replacingSchemaWatcher(stays as headless fallback and remains the only path for classpath watching); FSEvents native backend (deferred under R198’s out-of-scope list). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R198 (
b39d1c5lift SchemaWatcher FS-bound tests to synthetic dispatch,e10785cself-review: package-private dispatch + run()-time polling hint): lifts the fiveSchemaWatcherTestcases that depended on real-FS event delivery (modifyingGraphqlsFile_firesCallback,deletingGraphqlsFile_firesCallback,rapidWrites_firesCallbackOnce,newSubdirectory_isRegisteredAndFiresCallback, and oneCatalogRefreshTestcase) onto syntheticWatchEventvalues driven directly intoSchemaWatcher.dispatch. macOS’s JDK shipsPollingWatchServicewith a hardcoded 10 s period (sinceSensitivityWatchEventModifierwas removed in JDK 21), so the suite’s 1.6 s wait could never observe a real-FS event; the assertions the failing tests made were unit-tier invariants ondispatch(suffix filter, OVERFLOW reschedule, on-the-fly subdirectory registration) dressed up as integration tests of the JDK’s WatchService.writingGraphqlsFile_firesCallbacksurvives as the Linux-only inotify smoke (@EnabledOnOs(LINUX));nonGraphqlsFile_noCallbackdeleted as a duplicate ofdispatch_ignoresUnconfiguredSuffix;graphqlsWriteDoesNotFireClasspathWatcherfolded into the synthetic shape. NewwatchServiceBackend_matchesExpectedPerOsprobe pinsPollingWatchServiceon macOS andLinuxWatchServiceon Linux, so a future JDK shipping an FSEvents-backed WatchService would fail loudly and the Linux-only gate would get revisited. Runtime hint on the first iteration ofSchemaWatcher.run()emits twoLOGGER.infolines when the underlying WatchService is polling-based (JDK fact + LSP recommendation as separate lines so either can be revised independently); fires once per watcher lifetime in production, silent in synthetic-dispatch tests.SchemaWatcher.dispatchstays package-private; the lone cross-package consumer (CatalogRefreshTestin..maven.dev) routes through a new test-onlyDispatchTestSupportclass undersrc/test/java/…/maven/watch/, matching the existingwatchedDirs()test-seam precedent. Test counts:SchemaWatcherTest11→11,CatalogRefreshTest2→2. Out of scope (called out, not regressed): swapping the WatchService backend to a native FSEvents library (would add JNA to the plugin’s classpath; size separately); the LSP-driven regen path itself (R197); Linux aarch64 / Windows verification (R89). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R189 (
666f0fbadmit FK-target @nodeId input fields on @mutation,9524885In Progress → In Review): admitInputField.ColumnReferenceField/CompositeColumnReferenceField(FK-target@nodeId(typeName: T)pointing at another@table’s NodeType, classified to `Resolved.FkTarget.DirectFk) on every non-UPSERT@mutationverb. The reference carriers'liftedSourceColumnslive on the input’s own table, the extraction is narrowed toCallSiteExtraction.NodeIdDecodeKeys, and the emitters bind decoded keys againstliftedSourceColumnspositionally ; the same shape the same-tableColumnField/CompositeColumnFieldNodeId carriers already drive.MutationInputResolver.resolveInputdrops the deferred R24 rejection for these two carriers on INSERT / UPDATE / DELETE and removes the misleading "tracked in R24’s scope" hand-off; UPSERT stays refused at the kind gate (R145).InputField.LookupKeyFieldandInputField.SetFieldwiden permits to include both reference carriers. Load-bearing:EnumMappingResolver.buildLookupBindingsaddscase ColumnReferenceField/case CompositeColumnReferenceFieldarms emittingMapGroup/DecodedRecordGroupoverliftedSourceColumns(); without this themutation-input.where-columns-cover-pkcheck would silently under-count reference contributions and fire false "missing PK column" rejections on schemas whose FK column covers the PK.TypeFetcherGeneratorextracts five new helpers (emitSetMapPuts,emitSetExcludedPuts,emitSetVColNameAdds,emitSetBulkCellAdds,emitSetVFieldPutsplussetFieldColumns/setFieldNodeIdExtractiondispatchers) that replace eight(InputField.ColumnField) sfcasts overtia.setFields()across the UPDATE / UPSERT-SET / bulk-UPDATE paths; the INSERT-path helpers (anyNodeIdCarrier,buildInsertColumnList,buildPerCellValueList,buildInsertDecodeLocals) widen their carrier-shape switches with mirroring reference arms. Three@LoadBearingClassifierCheckannotations are restated:mutation-input.where-columns-cover-pk(semantic ; filter-column contributions now includeliftedSourceColumns()from the two reference carriers),mutation-input.update-set-fields-equal-value-marked(wording ; admissible-carrier set widened),mutation-input.lookup-binding-decoded-record-arity-matches-carrier-columns(wording ; arity guarantee extended to the FK-target composite arm). Tests: pipeline-tierMutationDmlNodeIdClassificationTestadds eight R189 cases ; arity-1 INSERT admission, arity-1 DELETE PK-coverage, arity-1 UPDATE with@valueSET field, composite-key DELETE throughreordered_pk_parent/reordered_fk_child, composite-key INSERT, and three bracketing rejection cases:fkTargetNodeIdRef_pkCoverage_underCount_negativeRejectionFixture(the load-bearing assertion that pins the validator widening ; without step 4 this exact shape would fire a false "missing: id_1" rejection that slips past both compilation and execution tiers),fkTargetNodeIdRef_pkCoverage_genuinelyMissing_rejected(contrast fixture confirming the canonical missing-PK rejection still fires), andfkTargetNodeIdRef_upsert_stillRejected_underR144(UPSERT refusal at the kind gate supersedes admission). Out of scope (called out, not regressed):Resolved.FkTarget.TranslatedFkadmission (the parent_node + child_ref shape where the FK targets a non-PK NodeType keyColumn ;BuildContext.java:1846-1849continues to produceInputFieldResolution.Unresolved); UPSERT (R145 territory); the output-side JOIN-with-projection NodeId encoding R24 originally hand-off pointed at. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R187 (
8e70b8cIn Review: nested @service arg-mismatch diagnostic at non-SOURCES shapes): the unresolved-@service-parameter discriminator inServiceCatalognow selects the arg-mismatch arm whenever the parameter type is not SOURCES-adjacent, instead of gating that arm onparentPkColumns.isEmpty(). Nested fields with a non-container parameter (LocalDate,String,Integer, …) whose name doesn’t match any GraphQL argument no longer fall through to "unrecognized sources type" ; they get the actionable "does not match any GraphQL argument or context key" hint with available args +argMappingsuggestion. The DTO-shape rejection arm is gated to nested coordinates only (!parentPkColumns.isEmpty()), preserving the root +List<DTO>→ arg-mismatch precedence pinned bydtoSources_onRootField_pointsAtArgCtxMismatch(this is the Spec’s "Precedence between DTO-hint and arg-mismatch" rule; the implementation deviates from Spec step-3’s literal wording, which would have moved the DTO arm up unconditionally and broken that test, but matches the Spec’s stated intent). Unit-tierServiceCatalogTestrewritesreflectServiceMethod_unrecognisedParam_onChildField_*(now asserts arg-mismatch ongetWithUnknown(Object)under non-emptyparentPkColumns) and addsreflectServiceMethod_nonSourcesPayloadOnChildField_pointsAtArgCtxMismatchpinning theLocalDatereproduction. Pipeline-tierGraphitronSchemaBuilderTestaddsSERVICE_ON_CHILD_WITH_NON_SOURCES_PARAM_NAME_MISMATCH_REJECTEDalongside the existing root-coordinate rejection case. Follow-up R193 (Backlog) captures the architectural smell flagged by the principles-architect review: the discriminator now has two consumers (R185 + R187) with subtly different precedence; a sealedUnresolvedParamclassifier would consolidate the precedence in one place. -
R185 (
b6539b9): narrowServiceCatalog.looksLikeSourcesShapetoList<RowN>/List<RecordN>only. A root@servicewhose Java parameter is aList<XRecord>(a concreteTableRecordsubtype) under a name that doesn’t match any GraphQL argument was getting the "`@service` at the root does not supportList<Row>/List<Record>/List<Object>batch parameters" diagnostic, shadowing the actionable arg-mismatch diagnostic that lists available argument names and suggestsargMapping.List<XRecord>at root is the canonicalInputBeanResolvershape, so a plain name typo collided with the Sources-shape exception. The concreteTableRecordbranch is removed fromlooksLikeSourcesShape; only the two anonymous-key shapes (RowN,RecordN) keep producing the Sources-batch diagnostic. The user-visible diagnostic also drops/List<Object>to match what now triggers it. Pipeline-tierSERVICE_AT_ROOT_WITH_TABLERECORD_PARAM_NAME_MISMATCH_REJECTED(GraphitronSchemaBuilderTest) asserts the arg-mismatch diagnostic wins forList<FilmRecord>under a mismatched name; the two existingRowN-element cases (SERVICE_AT_ROOT_WITH_SOURCES_PARAM_REJECTED,MUTATION_SERVICE_WITH_SOURCES_PARAM_REJECTED) keep passing and lock the predicate against opposite regression. -
R183 (
3ccd1eaGitLab pipeline targets graphitron-rewrite reactor on tags,bf5d2c3provision postgres service + run codegen + tests in publish,40790fcapt-get gcc so graphitron-lsp’s native build works,236860fparameterize test.db.url so CLI -D reaches surefire,dcdf0d2self-review cleanups before re-handoff): replaces the legacy reactor’s snapshot-on-default-branch + release-on-tag publish pipeline with a release-only pipeline targeting the rewrite reactor.publish:snapshotis deleted outright; default-branch pushes (including GitHub → GitLab mirror sync) no longer fire any deploy, eliminating the active hazard of legacy9-gitlab-SNAPSHOTartifacts accumulating in the Sikt GitLab Packages registry on every mirrored commit.publish:releasenow runsmvn -f graphitron-rewrite/pom.xml versions:set -DnewVersion=$VERSION -DgenerateBackupPoms=false -DprocessAllModules=true(theprocessAllModulesflag is load-bearing: without it the aggregator pom updates but child modules keep${revision}and the deploy publishes mismatched coordinates) followed bymvn -f graphitron-rewrite/pom.xml clean deploy -P gitlab,local-db -Ddb.url=jdbc:postgresql://postgres:5432/rewrite_test -Dtest.db.url=jdbc:postgresql://postgres:5432/rewrite_test. The tag regex widens to^v\d+\.\d+\.\d+(-RC\d+)?$to accept the-RC<n>suffix Maven Central consumers depend on. A newgitlabprofile ingraphitron-rewrite/pom.xmldeclares the GitLab Packages<repository>(no<snapshotRepository>; the rewrite parent’s invariant pergraphitron-rewrite/docs/README.adocPublishing: an accidentalmvn deployon10-SNAPSHOTmust fail fast) plusdeployAtEnd=trueand the sources-jar attachment; the root-pom legacygitlabprofile is unreachable from the new caller and dies with the legacy reactor under R182. The pipeline image bumps frommaven:3.9-eclipse-temurin-21tomaven:3.9-eclipse-temurin-25to satisfy the parent pom’srequireJavaVersionenforcer rule. In-runner Postgres:publish:releaseprovisions apostgres:18-alpineservice (aliaspostgres,POSTGRES_HOST_AUTH_METHOD=trust),apt-get install`s `postgresql-client+gccin the runner, and appliesgraphitron-rewrite/graphitron-sakila-db/src/main/resources/init.sqlviapsql. Thelocal-dbprofile is activated alongsidegitlabso jOOQ codegen reads against the live service rather than spinning up a Testcontainer (no Docker-in-Docker). Thegccinstall coversgraphitron-lsp’s `build-native.shinvocation atgenerate-resourcesthat compiles the tree-sitter native lib bundled into the publishedgraphitron-lspjar atlib/linux-x86_64/libtree-sitter-graphql.so. Surefire parameterisation:graphitron-sakila-example’s `local-dbprofile liftstest.db.url/test.db.username/test.db.passwordfrom literal<systemPropertyVariables>entries into pom<properties>(localhost defaults unchanged for local devs) so CLI-Dtest.db.url=…overrides reach surefire’s fork via property interpolation; without this, the CI’s overrides reached jOOQ codegen but the tests still triedlocalhost. Tests run in the publish pipeline as a deploy-boundary sanity check now that a real Postgres is in the runner anyway; the earlier "skip tests, GitHub gates them" rationale was load-bearing only when Docker-in-Docker was the alternative, and the GitHub publish workflow onmainis itself in flux per R182 so cannot be relied on as a gate. Out of scope (called out, not regressed): retiring the legacy reactor or the root-pomgitlabprofile (R182 collapses both); Maven Central publishing onmain(separate fix once R182 retires the legacy reactor; this item only touches GitLab); cleanup of existing junk9-gitlab-SNAPSHOTartifacts already deposited (manual GitLab UI task); arelease-clijob creating a GitLab Release object attached to the tag (cosmetic ; consumers depend by Maven coordinate). Verification deferred to a throwaway-tag push (e.g.v10.0.0-RC0) confirming the five expected coordinates deploy to GitLab Packages (no.sikt:graphitron-rewrite-parentpom,no.sikt:graphitron-javapoet,no.sikt:graphitron,no.sikt:graphitron-maven-plugin,no.sikt:graphitron-lsp) while the sixmaven.deploy.skip=truemodules stay out of the registry, and a default-branch push triggers no publish job. Build green locally: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 with thelocal-dbprofile pointing at a native Postgres. -
R178 (
9ee35d9step 1 DML-only cutover,e41ddb4step 2a wire transportForParent through selectErrorsTransport,64dfa33step 2b @service-carrier classifier wiring,fe270f9step 3 retire SettKvotesporsmal-bug carrier-walk consultations,c44193bPhase 4 slice 1 delete dead writers,7cbe4b6slice 2 retire transitional consultations,1211e6dslice 3 non-DELETE structural detection,e665e11slice 4 DELETE arm structural detection,1e71906slice 5 lift structural carrier scan to BuildContext,d0de975slice 6 retire verbless walk’s carrier-walk consultation,475e2ecslice 7a retire carrier-walk methods + load-bearing re-anchors,a1bbbcdslice 7b retire sealed model types,e06f1c8slice 7c retire SingleRecordIdentityField permit,83d2182slice 7d cosmetic,4a67b26self-review cleanup,1ddb22brename "Carrier" identifiers to "Payload",f42b819final self-review,575cd9bIn Progress → In Review): collapses the parallel single-record carrier walk to the unifiedSourceKey+ R96 reflection path. Deletes seven sealed hierarchies (SingleRecordCarrierResolution,SingleRecordCarrierShape,CarrierFieldRole,DataElement,BuildContext.tryResolveSingleRecordCarrieroverloads +classifyCarrierField,BuildContext.carrierProducerRegistry, the fourregister*CarrierDataFieldwriters,ChildField.SingleRecordIdentityField) and three load-bearing keys (single-record-carrier-shape.roles-exhaustively-classified,carrier-data-field.single-producer-kind,carrier-data-field.service-producer-strict-return). Adds two siblingProducerBindingarms (DmlEmitted(TableRef, DmlKind, Cardinality),ServiceEmitted(TableRef, Cardinality, producer-site)) grounded by R96 in dedicated memos, observed structurally on the payload SDL so the carrier walk’s forbidden-directives loop never fires at observation time (the SettKvotesporsmal bug’s mechanism). A builder-internal sealed resultBuildContext.DmlPayloadScan(Admit(dataField, DmlElementKind) | Reject(reason) | NotApplicable) replaces the carrier walk for the @mutation classifier andMutationInputResolver. The@fielddirective on a non-$sourcepayload data field no longer hard-rejects ; the SettKvotesporsmal contract pin: with and without@field(name:), semantically identical schemas classify identically. Themutation-dml-record-field.data-table-equals-input-tableinvariant re-anchors on the smallerrequireDmlDataTableMatchesInputTablehelper covering both DELETE and non-DELETE arms; theerror-channel.local-context-transportinvariant re-anchors onFieldBuilder.detectStructuralDmlErrorChannel(the new sole producer ofErrorChannel.LocalContexton DML payloads). The unit-tierErrorsTransportSelectionTestpins the errors-field defaulting rule table; the pipeline-tierSettKvotesporsmalShapeRegressionTestpins both the identical-classification and the diagnostic-wording contracts. The three payload-returning mutation permits (MutationDmlRecordField,MutationBulkDmlRecordField,MutationServiceRecordField) survive structurally; what changes is their classification path. Tests: unit-tierErrorsTransportSelectionTest(8 cases pinning every branch ofFieldBuilder.selectErrorsTransport); pipeline-tierSettKvotesporsmalShapeRegressionTest(3 cases: with-@fieldadmits, without-@fieldadmits identically, ClassBacked return-mismatch diagnostic cites the payload class not the inner record); pipeline-tierSingleRecordPayloadPipelineTest(renamed fromSingleRecordCarrierPipelineTest, 33 cases including R178 admits of@fieldand@deprecatedon the data field); execution-tierSingleRecordPayloadDmlTest(renamed, durability pins intact); theMUTATION_DML_RECORD_FIELD/MUTATION_BULK_DML_RECORD_FIELD/ DELETE-carrier /SINGLE_RECORD_IDENTITY_FIELD_ORPHANrows inGraphitronSchemaBuilderTestretarget to the unified path. Phase 5 deferral (called out in spec, separable follow-up): the emit-side migration toWrap.Row+Reader.ColumnReadships under R180 (record-parent-column-read-helper); Phase 5 survivors (ChildField.SingleRecordTableField,FromReturning,Reader.ResultRowWalk, the fiveFetcherEmitter.buildSingleRecordmethods, three remaining load-bearing keys) stay alive on the producer side until R180 lifts them. Out of scope (called out, not regressed):RecordBindingResolverwalk reshape (R178 adds one new producer arm but does not change the resolver’s model), R156’s NodeId encoder chain, two-step DML emit shape, wire-format serialization, the@table-parent child-classification path. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R184 (
05e21d5Graphitron.newExecutionInput factory + getContextArgument default,5d308c2In Progress → In Review): collapses the two pieces of per-request boilerplate every graphitron app repeats (.graphQLContext(b → b.put(GraphitronContext.class, ctx))to thread the typed context key generated fetchers look up, plus.dataLoaderRegistry(new DataLoaderRegistry())to satisfy graphql-java’s always-required registry) into a single generated factory entry point on the emittedGraphitronfacade.GraphitronFacadeGeneratornow emits two staticnewExecutionInputoverloads alongsidebuildSchema:newExecutionInput(GraphitronContext context)returnsExecutionInput.newExecutionInput().graphQLContext(b → b.put(GraphitronContext.class, context)).dataLoaderRegistry(new DataLoaderRegistry()), and the single-tenant conveniencenewExecutionInput(DSLContext dsl)delegates tonewExecutionInputGraphitronContext) env → dsl). To make the lambda form bind toGraphitronContext’s SAM rather than infer as `Function<DataFetchingEnvironment, DSLContext>,GraphitronContextInterfaceGeneratorflipsgetContextArgumentfromABSTRACTtoDEFAULTwith bodyreturn env.getGraphQlContext().get(name);(matching the legacyDefaultGraphitronContext);getDslContextis now the only abstract method on the interface. The interface is deliberately not annotated@FunctionalInterface(a permanent contract that would block ever adding another abstract method); instead a pipeline-tier test pins the count of abstract methods to one, so any future generator change that adds a second abstract method fails the test in tandem with the sakila example’s compile of the lambda form. Tests: pipeline-tierGraphitronFacadeGeneratorTestasserts the twonewExecutionInputoverloads exist with(GraphitronContext)and(DSLContext)parameter lists, both returninggraphql.ExecutionInput.Builder, bothpublic static; pipeline-tierGraphitronContextInterfaceGeneratorTestassertsgetContextArgumentcarriesDEFAULT(notABSTRACT) with the right body and that the emitted interface has exactly one abstract method; compilation-tierNewExecutionInputFactoryTestingraphitron-sakila-examplepins three graphql-java contract facts (.dataLoaderRegistry(custom)replaces the factory’s fresh registry rather than merging ; exercises the user-visible override path; the(DSLContext)overload defaults to an empty registry; the(GraphitronContext)overload places the context under the typedGraphitronContext.classkey generated fetchers read from). Execution coverage comes from the sakila example’s rewrittenGraphqlResource.execute: the six-line builder collapses to three viaGraphitron.newExecutionInput(new AppContext(…, and every existing execution test ingraphitron-sakila-exampleruns through that method. Docs updated to point at the factory (getting-started.adochello-world + multi-tenant + DataLoader-registry sections;runtime-extension-points.adocregistration snippet; emitted facade + interface javadocs). Out of scope (called out, not regressed): no federation overload ofnewExecutionInput(ExecutionInput carries no federation-specific wiring; the same factory serves both schema flavours); no change to DataLoader registration mechanics (generated fetchers continue to populate the registry lazily viacomputeIfAbsent); no new module, no change to dependency graph;getTenantIdandgetValidatordefaults unchanged (they were already default-method shaped). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R179 (
ef0af6cdelete ResultAssembly; service success arm is universal passthrough,a3bde69self-review cleanup: retire stale ResultReturnType coverage claim): deletes theResultAssembly+ResultSlotmodel types, theresolveServiceResultAssembly+buildResultAssemblyBeanArmclassifier inFieldBuilder, and thebuildSuccessPayload/buildSuccessPayloadCtor/buildSuccessPayloadSettersemit helpers inTypeFetcherGenerator. The success arm collapses to a single universal-passthrough body:T result = service.method(…); return success(result);. The architectural constraint the deletion enforces is that the generator does not construct output DTOs on the happy path: per-field wiring (graphql-java child fetchers) projects SDL fields off the parent’s domain return, and a SDL-declaredCreateFilmPayload { film: Film, errors: [Error] }does not need a Java twin. The catch-armpayloadFactoryLambdais the only remaining DTO-construction site, and the boundary is structural ; error-routed lists are produced inside the generator-owned try block where per-field wiring has no parent value to project from. The four service-backedFieldrecords (Query/Mutation × Table/Record) lose their trailingOptional<ResultAssembly> resultAssemblycomponent; the four arms inclassifyQueryField/classifyMutationFieldroute throughbuildServiceField, whoseBiFunction<channel, assembly, …>collapses toFunction<channel, …>. A new surviving classifier check (checkServiceReturnMatchesPayloadinFieldBuilder) replaces the three Assembly-specific reject messages with the single legacy-passthrough wording:"@service method '<cls>.<method>' must return '<sdlPayloadTypeName>' to match the field’s declared payload type ; got '<method.returnType()>'". Stale-doc cleanup spans 12 sites (FieldBuilderLoadBearingClassifierCheck descriptions ×2 +ServiceCatalog.reflectServiceMethoddescription retired ResultReturnType coverage claim,FieldBuilderjavadoc ×4, model-class javadocs onDefaultedSlot/PayloadConstructionShape/NonBoundSetter/ErrorsSlot,ServiceDirectiveResolver/MutationInputResolverResultReturnTypearm comments,SettKvotesporsmalShapeRegressionTest+TestServiceStubjavadoc). R169 (service-domain-object-execute-coverage, Backlog) deletes in the same commit per workflow.adoc’s "supersession before shipping" Discarded pattern: its entire scope was execute-tier coverage for the now-deleted Assembly arm. One fixture addition beyond the spec’s enumeration:TestServiceStub.runSakWithInputBean(TestInputBean)returningSakPayload, replacing the String-returning stub that the pre-existingFetcherPipelineTest.inputRecord_validatorPreStep_*test had relied on the Assembly arm to admit; the new stub preserves the test’s intent (validator pre-step on Input-typed arg with VALIDATION-bearing channel) without losing the R150 input-bean classification dimension.LoadBearingGuaranteeAuditTestnet stays balanced: bothpayload-construction.producers retain surviving consumers (catch-armpayloadFactoryLambda+ validator pre-stepdeclareEarlyPayloadFromErrors/declareEarlyPayloadSetters); no producer becomes orphaned. *Out of scope (called out, not regressed): inlining the success-arm local (kept for catch-armtry-block uniformity); redesigning the catch-armpayloadFactoryLambdapath (the only remaining DTO-construction site, principled and unchanged). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R177 (
1aba97fchild @service rows-method preserves specific XRecord type,de5ed05self-review: name the third consumer in RowsMethodShape class doc): narrows the child-@serviceemit site so the rows-method’sVand the matchingDataLoader<K, V>value type both honor theTableBoundReturnTypeclassifier guarantee instead of widening to raworg.jooq.Record.RowsMethodShape.strictPerKeyTypereturnstb.table().recordClass()for theTableBoundReturnTypearm (was: aRECORDconstant, now deleted);TypeFetcherGenerator’s `ChildField.ServiceTableFieldarm threadsstf.returnType().table().recordClass()as the singleservicePerKeyTypelocal into bothbuildServiceDataFetcher(theDataLoader<K, V>typing line) andbuildServiceRowsMethod(theMap<K, V>/List<V>return-type line), so the typed loader populates from the rows-method without a wildcard or defensive cast.ServiceDirectiveResolver.validateChildServiceReturnType’s `@LoadBearingClassifierCheckdescription is rewritten to name both emit-site consumers explicitly (rows-method.returns(…)and the typedDataLoader<K, V>), capturing that the strictTypeName.equalsarm is now load-bearing for the typed loader’s compile via Java generics invariance ; not just structural symmetry; a new@DependsOnClassifierCheckonbuildServiceDataFetcherpins the data-fetcher side to the same key, and the existing annotation onbuildServiceRowsMethodis updated to reflect the narrower V.RowsMethodShape’s class-level docstring lifts the consumer count from two to three and names the DataLoader-typing line with the Java-generics-invariance reason. The diagnostic wording on the validator’s strict-return rejection shifts from `must return 'List<Record>'tomust return 'List<LanguageRecord>'(the specific record class for the field’s bound table). Tests: sixServiceTableFieldunit assertions inTypeFetcherGeneratorTestflip fromorg.jooq.Recordto the specificFilmRecord(positional + mapped, single + list, on both data-fetcher and rows-method return); three new R177-axis enum rows inGraphitronSchemaBuilderTestpaired with three new fixtures inTestServiceStubcover the migration arm (List<List<Record>>was accepted, now rejected), acceptance arm (List<List<LanguageRecord>>was rejected, now accepted), and cross-record regression (List<List<FilmRecord>>stays rejected);CHILD_SERVICE_TABLE_BOUND_WRONG_RETURN_REJECTED’s diagnostic assertion updated to the narrowed wording; pipeline-tier `TestFilmService.getFilmstightened toList<List<FilmRecord>>with the paired pipeline assertion flipped; new positive compile-tier fixtureFilm.languageByServiceingraphitron-sakila-examplebacked byFilmService.languageByServicereturningMap<Record1<Integer>, LanguageRecord>makesmvn compile -pl :graphitron-sakila-examplethe load-bearing guarantee against future re-widening of the emit site. Out of scope (per spec, not regressed):ChildField.ServiceRecordField’s `elementType()fallback path (the asymmetry is principled ;ServiceRecordFieldcarries the broadReturnTypeRefsealed root rather thanTableBoundReturnType, andstrictPerKeyTypecan return null for that variant); theSourceKey.Wrap.TableRecordsource-side typing pipeline (R177 brings the target side into alignment with what the source side already does); the rows-method’s outer container shape (MapvsList, single vs list cardinality ;outerRowsReturnTypecontinues to wrap whateverperKeyit’s handed);SplitRowsMethodEmitter(theChildField.SplitTableFieldarm stays rawRecord, explicit non-goal). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R96 (
ab18e8cProducerBinding sealed taxonomy + RecordBindingMismatch rejection,d098d76RecordBindingResolver ; reflection-driven SDL → backing-class walker,bc1e457wire RecordBindingResolver into TypeBuilder + reflection-derived dispatch,2a19d40transitional dispatch: gate result-axis walker on@record+ directive fallback,37ee435flatten RecordBindingMismatch to RecordBindingMultiProducer + doc coverage,4127f3dpipeline-tier tests for record-binding behaviour,ec229f1self/arch review fixes): replaces the directive-drivenrecordBackingClassespopulation inTypeBuilderwith a reflection-driven walk that grounds at root producers (@servicereturns,@tableresolutions,@tableMethodreturns) and propagates through parent-accessor return types to a fixed point.RecordBindingResolver.resolveAll()accumulates every observed binding per SDL type into a collection set keyed on either axis (result / input); the per-type set folds at the end of the walk into either an agreedClass<?>(singleton) orRejection.AuthorError.RecordBindingMultiProducer(more than one distinct class). Cycle protection is fixed-point iteration over the per-type collection sets rather than recursive descent: each pass snapshots the currently-folded bindings, walks each parent’s accessor edges (getX/isX/x/ public field-read, with optionalDataFetchingEnvironmentparam), and adds new (reflectedClass, site) pairs to the per-type sets until a pass produces no new entry; a 1000-pass safety bound surfaces non-convergence asIllegalStateException. The new sealedProducerBindingtaxonomy (RootService/RootTable/RootTableMethod/ParentAccessor) carries the typed list inside the rejection so downstream tooling switches on the arm rather than parsing prose. Three-variant directive-ignored warning emitted at a single post-classification site (TypeBuilder.emitDirectiveIgnoredWarnings):Matches("redundant; remove it") when the directive’sclassNameequals the reflected class or the directive carries noclassName;Disagrees("graphitron derives <X>") when they differ, naming the reflected class;Shadowed by @table(input types only) when@tableco-occurs and grounds the binding. Variant precedence: Shadowed > Matches/Disagrees; a multi-producer rejection suppresses the warning entirely (error supersedes warning at the same site). The legacy@table + @recordinput-side warning atTypeBuilder.java:826-831is removed; the redundancy signal is now carried by theShadowed by @tablevariant. Load-bearing pin: producer@LoadBearingClassifierCheck(key = "record-binding.producer-agreement")onRecordBindingResolver(description names the two pure-function commitments riding under the check:ServiceCatalog.resolveTableByRecordClassderivesTableRefpurely fromcls, and a Java record’s component list is a pure function ofcls) pairs with@DependsOnClassifierCheckonFieldBuilder.resolveRecordAccessor. Tests: pipeline-tierR96RecordBindingPipelineTest(six cases: Matches with@serviceproducer; Disagrees with directive lying about class; Shadowed-by-table on input; unreachable type falls back to directive’s className for backward compat; plain SDL carrier preserved through R75’sPojoResultType.NoBackingpromotion; multi-producer disagreement surfaces typedRecordBindingMultiProducerrejection);RejectionSeverityCoverageTestadds the new permit to its sample factory;GraphitronSchemaBuilderTest’s `TABLE_PLUS_RECORDandSERVICE_WITH_RECORD_BACKING_CLASS_MISMATCH_REJECTEDcases update to assert R96 semantics (table wins on input + Shadowed-by-table warning; service-with-record-mismatch corrects silently with Disagrees warning instead ofUnclassifiedField);ErrorChannelClassificationTest.unTypedRecordPayload_*flips from "produces no channel" to "produces channel from reflected producer" ; a correctness improvement R96 introduces because@recordwithoutclassNamenow grounds via the producer’s return type rather than falling through toNoBacking;SealedHierarchyDocCoverageTestpasses against the updatedtyped-rejection.adoc(new fourthAuthorErrorarm documented at chapter prose + mermaid class diagram). The sakila/test fixture corpus emits the expected Matches warnings (CreateFilmPayload,CreateFilmsPayload,CustomerAddressSummary,FilmLookupPayload,FilmReviewPayload,SetterShapeFilmReviewPayload) ; every@record-decorated reachable type whose directive’sclassNamematches the producer’s return is now flagged as redundant. Transitional state (explicitly captured in the spec’s "Implementation notes" appendix before deletion, with the named follow-on tracked separately): walker’s@serviceresult-axis observation is gated on the SDL return type carrying@record(preserves R75 single-record-carrier semantics; the post-retirement anchor isBuildContext.tryResolveSingleRecordCarrier, and the follow-on must flip the gate atomically with directive retirement);@tableMethodarm contributes input-axis observations only (the@tableobservation alone is sufficient for the result axis; obtaining theTable<Record>reflection class viaTableImpl.recordType()for bare-class returns would add machinery without strengthening the diagnostic);buildResultType/buildNonTableInputTyperetain a directive-className fallback for types the walker can’t reach (so existing fixtures classify without mass migration); the rejection lands as a singleRejection.AuthorError.RecordBindingMultiProducerpermit directly underAuthorErrorrather than a two-levelRecordBindingMismatch.MultiProducersub-taxonomy (the flat shape matches the rest of theAuthorErrorfamily and the typedList<ProducerBinding>payload already carries what a sub-arm would key off; theSealedHierarchyDocCoverageTest’s qualified-mention regex collides on two-level prefixes ; a known doc-coverage tooling fix tracked as a follow-on so future shapes that need sub-taxonomies aren’t blocked); drop-manifest golden file, validator-tier tests on the warning’s three variants, and the synthetic accessor-graph unit test on the resolver (diamond / deep chain / grounded cycle / ungrounded cycle) are deferred to the follow-on item, which will retire the directive-fallback path, migrate the remaining test fixtures, and ship the `directives.graphqls:290directive declaration retirement atomically with the walker’s@record-gate flip once the warning count drops to zero across the corpus. Out of scope (per spec, not regressed): retiring the directive declaration itself; retiring any of the eight backed model variants (R96 changes the binding source, not the destination); the@service-payload error-construction surface (payloadFactoryLambda,ResultAssembly,PayloadAccessor); R94’s input-record validation seam (recordShapeslot on the fourInputTypepermits is graphitron-emitted validation class; R96’srecordBackingClassesis author-supplied accessor target ; orthogonal axes). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R176 (
8292468preserve upstream rejection in EntityResolutionBuilder): replaces the misleading@key requires a @table-bound type; '<T>' has no @table directivesynthesised atEntityResolutionBuilder.java:108-114for any non-Table/Node classification with two call-site-specific behaviours that codify the principle downstream stages enrich an upstream rejection or pass it through; they do not relitigate it. WhengType instanceof UnclassifiedType(the type was already rejected upstream byTypeBuilder.unknownTableRejection, the@nodekeyColumnsunresolved-column check, malformedKjerneJooqGeneratornode-id metadata, or@nodedeclared on a type withoutimplements Node), the new code skips the demote outright and letsGraphitronSchemaValidator.validateUnclassifiedTypesurface the original cause unmodified. WhengTypeis a genuine non-table-bound classification surviving theassembledType instanceof GraphQLObjectTypeandTableInterfaceTypepre-checks (today:PlainObjectTypeand theResultTypesub-hierarchyJavaRecordType/PojoResultType/JooqRecordType/JooqTableRecordType), the demote fires with a kind-aware rejection:@key on type '<T>' requires a table-bound type, but '<T>' is classified as <kind> ; federation entities need a @table directive.where<kind>is supplied by a new privatekindLabel(GraphitronType)switch (PlainObjectType→"a plain object type", the fourResultTypevariants →"a @record type", default branch →"a non-table-bound type"for any future classification a contributor adds without updating the switch). The three legitimatedemotecallers (TypeBuilder.java:226typeId collision,EntityResolutionBuilder.java:104@keyonTableInterfaceType,EntityResolutionBuilder.java:128alternative-build error) all demote from classified entries and stay as-is; only the bug call site is gated. Why call-site, not aTypeRegistry.demote-refuses-overwrite invariant: rejection durability is the caller’s responsibility because only the caller knows whether it’s enriching a rejection the classifier couldn’t see (legitimate) or relitigating one (the bug); the registry can’t tell those apart from the type signatures alone, so the discipline lives at the call site where the knowledge lives. Tests:EntityResolutionBuilderTestadds three regression cases pinning the new behaviour (keyOnTypeWithUnresolvableTable_preservesUnknownTableRejectionasserts the rejection containscould not be resolved in the jOOQ catalogand does not containhas no @table directive;keyOnNodeTypeWithUnresolvableKeyColumn_preservesUnresolvedColumnRejectionasserts the rejection containskey column 'definitely_not_a_column' in @node could not be resolvedand the same absence;keyOnRecordType_namesRecordKindInMessageasserts the rejection containsis classified as a @record typeand the same absence), and tightens the existingplainObjectTypeWithKey_demotesToUnclassifiedTypefrom a loosecontains("@table")to require bothis classified as a plain object typeandfederation entities need a @table directive. Out of scope (called out, not regressed): surfacing all rejections per type rather than the first (the validator’s one-error-per-UnclassifiedTypepolicy is unchanged); LSP fix-it hints for the new wording (the structuralRejectioncarries enough payload for an LSP layer to consume later); changingTypeRegistry.demotesemantics. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R94 (
1224167scope down emit shape to class-not-record (R172/R174 follow-ons filed),418ef97ship per-SDL-input-type validation class + rewired validator pre-step,e86c856self-review cleanup: drop dead two-argInputRecordGenerator.generateoverload, simplify nested-inputfromMapto one statement, refresh "Java record" → "Java class" Javadoc with R174 forward-looking notes): emits one graphitron-internal Java class per reachable SDLinputtype at<outputPackage>.inputs.<InputName>and rewires R12’s validator pre-step atTypeFetcherGenerator:1602+to walk the typed instance instead of the rawMap. NewHasInputRecordShapecapability interface declared on the fourInputTypeleaves (PojoInputType,JavaRecordInputType,JooqRecordInputType,JooqTableRecordInputType) and onTableInputType;InputRecordShapecarries(recordClass, List<InputComponent>)with a compact constructor that rejects null/empty and backs theinput-record.shape-from-input-type@LoadBearingClassifierCheckkey.TypeBuilder.buildInputRecordShapewalks SDL fields and lifts scalars via R101’sScalarTypeResolver, enums toString(graphql-java’s wire shape), nested input refs toClassName.get(<outputPackage>.inputs, name)(forward-declared ; javapoet does not require the class to exist at codegen, so mutually recursive inputs resolve cleanly), and lists toList<X>; a field whose scalar fails to classify routes the parent throughUnclassifiedTypevia the existing fail-mode.InputRecordGeneratorwalks the reachable input closure off the assembledGraphQLSchema’s `GraphQLObjectTypefields (the rewrite model’sRootType/TableBackedTypedon’t carryschemaType(), so the assembled schema is the authoritative source), expanding transitively through nested input components; each emitted class ispublic finalwith one private field per SDL component, a public same-name accessor, a private canonical constructor, a staticfromMap(Map<String,Object>)factory (nested-input components recurse the sibling factory; list components stream element-wise; scalars/enums direct-cast; symmetric-null contract ; absent key and explicitnullboth collapse to a null component), and a per-class Javadoc tagging it as a graphitron-internal validation target."inputs"is added toGraphQLRewriteGenerator.OWNED_SUBPACKAGESso the orphan sweep cleans regressions.TypeFetcherEmissionContextgrowsassembledSchema()+parentTypeName()so the rewired validator pre-step inTypeFetcherGeneratorcan resolve each SDL arg’s input-type-ness without re-walking the schema per arg; input-typed args materialise via<InputName>.fromMap(env.getArgument(name))and feedvalidator.validate(<typed>). Scalar/enum args stay on the raw value path. Class-not-record by deliberate scope-down:graphitron-javapoetdoes not currently supportTypeSpec.Kind.RECORD,sealed/permitsclauses, orpackage-info.javaemission (covers onlyCLASS,INTERFACE,ENUM,ANNOTATION); R174 (javapoet-record-sealed-package-info-support, Backlog) tracks the framework upgrade ; once it lands,InputRecordGeneratorre-emits as actual records + sealed marker + package-info with no model-side ripple (InputRecordShape/InputRecordGeneratorkeep their names and semantics, only the renderedTypeSpecshape changes). The structural enforcement seam reduces to package boundary + per-class Javadoc for R94; R172 (inputs-package-internal-use-audit, Backlog) ships the service-side-reference audit independently. Hibernate Validator 9.0.1 walks records and beans identically for the property-path-from-component-name purpose R12’sConstraintViolations.toGraphQLErrorneeds, so the validator-walk function R94 delivers is preserved despite the source-form difference. Load-bearing pin: producer@LoadBearingClassifierCheck(key = "input-record.shape-from-input-type")onTypeBuilder.buildInputRecordShapepairs with@DependsOnClassifierCheckonInputRecordGenerator(per-input-type class emission) andTypeFetcherGenerator.validatorPreStep(typed-record materialisation in the pre-step rewire). Tests: pipeline-tierFetcherPipelineTestadds five R94 cases (inputRecord_scalar_emitsFromMapAndValidatesAgainstRecord,inputRecord_list_emitsListComponent,inputRecord_nested_recursesCoercer,inputRecord_unreachable_emitsNoRecord; pins the reachable-closure scope decision via an unreachable input that produces no class ; andinputRecord_validatorPreStep_receivesTypedRecordNotMap; the regression guard against drifting the pre-step back tovalidator.validate(Map)); unit-tierInputRecordGeneratorTestcovers the emit shape (public class in<outputPackage>.inputs,fromMapfactory withMap<String,Object>signature, one accessor per SDL component);InputTypeValidationTestgains aplaceholderShape()helper since it constructsPojoInputTypedirectly without the classifier. The validator pre-step walks the empty record (no constraints attached yet) ; the shape of the record (components,fromMapsignature, walk-target) is exercised end-to-end on every fetcher with an input arg, so R98’s later content-attachment (programmaticConstraintMappingentries) doesn’t have to reshape the record. R170 picks up the live invalid-input round-trip the moment R98 ships its first SDL constraint. Unblocks R98 (multi-source-input-validation, Backlog) ; the mergedConstraintSet’s programmatic-registration consumer gets its `mapping.type(InputRecord.class).field(componentName)…target ; and R170 (validator-integration-execute-coverage, Backlog) ; R12’s pre-step gains a real annotated walk target for the execute-tierConstraintViolationround-trip fixture. Out of scope (called out, not regressed): exposing emitted classes to service signatures (R150 owns@servicevalue flow via consumer-authored beans; the graphitron class is a validation target only and is discarded aftervalidator.validatereturns); replacing theMap.get()pattern in DML emitters (the fourbuildMutation{Delete,Insert,Update,Upsert}Fetcherpaths and R75/R161-shipped DML-record paths keep their current shape ; the validate-only class runs in parallel at the fetcher boundary, value reads stay on the Map); destructuring@servicecallsites (R150 owns); service-sidevalidator.validatecalls (validation is a fetcher-boundary concern; the service never sees the graphitron class); designing the SDL validation directive set (R98 owns the curated@Range/@Size/ etc.); narrowing/deprecating/removing@recordonINPUT_OBJECT(R96 owns the reflection-derived backing-class binding; R94’s graphitron-emitted class at<outputPackage>.inputs.<InputName>lives at a separate Java identity from whatever class@recordbinds the input type to); retiring the fourGraphitronType.InputTypevariants (R96 keeps the variants and reshapes how they’re populated); the@table + @recordshadow rule atTypeBuilder:815-824(untouched). Forward references: R164 (field-model-two-axis-pivot, Backlog) will repoint the validator-pre-step dispatch site intoValidationBuilder.OnInput-arm pattern matching once the field-model pivot lands; the substance of the pre-step (call<InputName>.fromMap, thenvalidator.validate) is unchanged ; R94’sInputRecordShape/InputComponentare type-side (attached toGraphitronType.InputType), andValidationBuilderis field-side (attached toField), so the two axes don’t compete. R171 (input-like-type-sealed-parent, Backlog) tracks foldingInputType ∪ TableInputTypeunder a sealedInputLikeTypeparent so the capability declaration becomes one site instead of five; until R171 lands, a future sixth input-like variant added toGraphitronType.permitswill not get a compile-time miss forHasInputRecordShape. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 (1763 graphitron module tests + sakila compile-tier + execute-tier). -
R9 (
a4675bfPhase 1 pipeline,c38ea0fPhase 2 in-repo .md → .adoc,562e732Phase 3 alf/graphitron-landingsside absorption,aa3511ePhase 4 roadmap/by-theme/changelog/plans render,7abec54Phase 5a custom-domain cutover,b824207In Review):graphitron.sikt.nois now built by Maven and deployed to GitHub Pages./docs/is apom-packaged Maven module (graphitron-docs) wired into the rewrite reactor via<module>../docs</module>;download-maven-pluginfetches@sikt/sds-coreand@sikt/sds-buttonfrom the npm registry tarball at pinned versions,maven-antrun-pluginflattenspackage/dist/index.cssintotarget/staging/css/sds-{core,button}.css,maven-resources-pluginstages authored/docs/.adocplus/graphitron-rewrite/docs/.adocunderarchitecture/,roadmap-tool render-adocemitsroadmap/{index,by-theme,changelog,plans/<slug>}.adoc, andasciidoctor-maven-plugin(in the default-ondocsprofile, opt-out via-P!docs) renders the merged tree totarget/generated-docs/withfailIf severity=WARNso missing xrefs, missing includes, and unresolved attributes fail the build. CI splits across three workflows:rewrite-build.ymlbuilds the rewrite reactor on PR and trunk push and adds trunk-onlydocs-build/docs-deployjobs (actions/upload-pages-artifact+actions/deploy-pages@v4,concurrency: { group: pages, cancel-in-progress: false });preview-docs.ymlbuilds the docs site on every PR touchingdocs/,graphitron-rewrite/docs/, orgraphitron-rewrite/roadmap/and uploads the rendered tree as a workflow artifact; the legacymaven-build.ymlstays unchanged on Java 21. Sikt Design System integration is build-time (pinned<sds.core.version>/<sds.button.version>properties, npm-registry tarball preferred over JSDelivr after the Claude Code Web sandbox returned 403host_not_allowed), so the deployed Pages site has no runtime third-party dependency;sds-core’s `LICENSE.mdis copied intotarget/staging/css/for attribution, intra-Sikt scope on Sikt-owned domain authorises the redistribution. The custom-domain cutover (Phase 5a,7abec54) dropped the/graphitron/path-prefix Phase 1 anticipated; Sikt platform team handled DNS and the Pages-settings custom-domain config; no/docs/CNAMEfile shipped (custom domain set via Pages settings, the standard mechanism withactions/deploy-pages). Phase 5b (K8s deployment retire, GitLab CI pipeline retire,alf/graphitron-landingssidearchive) landed external to this repo and is confirmed complete. *Done-commit housekeeping* (<this commit>): cleared four user-facing-doc-check leaks the independent In Review reviewer (session_011jbm5PpFDrqu3WjhtXDFB4) surfaced ;docs/index.adoc:69dropped theR68 scaffold-onlysecond sentence (the manual is shipped and populated),docs/manual/reference/directives/externalField.adoc:88anddocs/manual/how-to/computed-fields.adoc:152reframed thecomputed-field-with-reference.mdplan-slug references to feature-status notes,docs/manual/reference/directives/value.adoc:39softened "deferred to a follow-up roadmap item" to "UPSERT generation is deferred". Pre-existing drift inCLAUDE.md:74(Documentation site section) anddocs/README.adoc:6rewrote the stale.github/workflows/deploy-docs.ymlreference to point at thedocs-build/docs-deployjobs inrewrite-build.ymlpluspreview-docs.ymlfor PR previews. Build green:mvn -f graphitron-rewrite/pom.xml -pl :graphitron-docs -am packagerenders the merged tree under the WARN-fails policy on Java 25. -
R167 (
ee06817unify schema file extension handling between schemaInputs and graphitron:dev): centralises the "what counts as a schema file" decision onto a single<schemaFileExtensions>Mojo parameter and threads it throughRewriteContext.schemaFileExtensions(): Set<String>to three consumer sites that previously drifted independently.SchemaInputExpander.expandpost-filters scanner matches by extension;SchemaWatcher’s schema-mode constructor takes the `Set<String>instead of a hard-coded.graphqls;SchemaProblemDiagnostic.findOrphanSchemaFilesreads the configured set instead of its own hard-coded.graphql/.graphqlspredicate. The seven- and six-argRewriteContextoverloads default the new field toSet.of(".graphqls", ".graphql")so unit-tier callers stay one-liners;AbstractRewriteMojo.effectiveSchemaFileExtensions()is the normalisation seam (trim, leading-dot prepended, duplicates collapsed, empty-after-normalisation rejected with aMojoExecutionException). The default matches the orphan scanner’s pre-R167 behaviour, so consumers with.graphqlfiles (Opptak’sregelverkMutations_exp.graphqlwas the concrete pain point) get thegraphitron:devwatcher firing on save and the<schemaInputs>glob-expansion picking the file up without configuring anything; teams reserving.graphqlfor client query documents opt in to the tighter<schemaFileExtensions><extension>.graphqls</extension></schemaFileExtensions>policy.SchemaWatcher’s single-string-suffix constructor stays for the `.classclasspath watcher (DevMojo.startClasspathWatcherstill passes".class"). Tests: unit-tierSchemaFileExtensionsNormaliserTest(6 cases: null returns default, missing-dot prepended, duplicates collapsed, whitespace trimmed, all-blank rejected, explicitly-empty rejected);SchemaInputExpanderTestaddsexpand_filtersFilesNotMatchingConfiguredExtensions,expand_dotGraphqlAccepted, andexpand_zeroMatchAfterExtensionFilter_throwsMojoExecutionException;SchemaWatcherTestaddsdispatch_triggersOnDotGraphql_whenConfigured,dispatch_ignoresUnconfiguredSuffix, andconstructor_emptySuffixSet_rejected;SchemaProblemDiagnosticTestaddsfindOrphanSchemaFiles_respectsConfiguredExtensions(tighten + loosen both checked).docs/manual/reference/mojo-configuration.adocdocuments the new parameter in the per-parameter reference table and updates thedevgoal description; the<schemaInput>row prose softens from.graphqlsfiles to "schema files (or globs that expand to schema files)". Out of scope (called out, not regressed): dropping the trailing/*.graphqlsoff<pattern>declarations so patterns describe directories only (purely additive on existing patterns); case folding (Linux is case-sensitive,.GraphQLsstays as authored); bundled directive files (directives.graphqlsis a classpath resource, not consumer-configured). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R158 (
f35644fadmit@service-backed producers for single-record DML carrier data fields;da25606pipeline + unit tests;129909cexecution-tier single-PK + ONE + empty + null-source cases;08acc84execution-tier MANY-arm composite-PK case): widens the single-record DML carrier’s data-field permit to admit a second producer kind alongside the existing INSERT/UPDATE/UPSERT DML mutation: an@service-backed mutation whose return type IS the carrier-payload’s typedXRecord/List<XRecord>. Closes the runtimeArrayList cannot be cast to org.jooq.Resultreproducer (OpprettRegelverksamlingPayload+opprettRegelverksamling @servicereturningList<RegelverksamlingRecord>): pre-R158 the data-field fetcher castenv.getSource()toResult<RecordN<PK>>unconditionally, which holds for the DML mutation fetcher’s.returningResult(PK)shape but not for the developer’s verbatim list-of-typed-record return.SourceKey.Reader.ResultRowWalk’s compact-constructor invariant widens from `Wrap.Record + empty pathtoWrap.Record OR Wrap.TableRecord(target.recordClass()) + empty path; the load-bearing key renames in lockstep tosource-key.result-row-walk-target-aligned-empty-path(one@LoadBearingClassifierCheckonSourceKeyand one@DependsOnClassifierCheckonFetcherEmitterupdated, plus two javadoc references onChildField.java/SourceKey.java).FetcherEmitter.buildSingleRecordTableFetcherValuebecomes a sealed switch overSourceKey.Wrappermits: theWrap.Recordarm preserves the existing(Result<RecordN<…>>) env.getSource()/(RecordN<…>) env.getSource()casts andsource.getValues(<PK>)/source.value1()reads unchanged; the newWrap.TableRecordarm castsenv.getSource()to(List<XRecord>)(MANY) or(XRecord)(ONE) and reads PKs through the typedrecord.get(<XTable.<PK_FIELD>>)accessors (single-PK uses the column’s Java type as map key; composite-PK usesList.of(r.get(pk1), r.get(pk2), …)for map-keying andDSL.row(pk1, pk2).in(source.stream().map(r → DSL.row(…)).toList())for the response predicate);Wrap.Rowis the unreachable arm pinned by anIllegalStateException. Registration moves to per-producer helpers.GraphitronSchemaBuilder.registerCarrierDataField’s `DataElement.Tablearm hollows out; two new helpers inFieldBuilderare the only writers:registerDmlCarrierDataField(called from the non-DELETE DML kind classifier withWrap.Record) andregisterServiceCarrierDataField(called from theResolved.Resultarm of@serviceresolution withWrap.TableRecord(target.recordClass())). The@servicehelper does its own strictmethod.returnType().equals(expectedReturnType)check against the carrier walk’starget.recordClass(), colocated becauseServiceDirectiveResolver.computeExpectedServiceReturnTypereturnsnullfor carrier-payload return types by design. Orphan carriers (a carrier type returned only from a Query field with no producing mutation) now land with nofieldRegistryentry, structurally safe under graphql-java’s never-traverse-an-unproduced-field guarantee. R156’sregisterDeleteCarrierDataFieldTable arm passesnullforexpectedExistingClassin lockstep because the verbless walk no longer pre-registers;FieldRegistry.reclassify’s `expectedExistingClassparameter loosens to admitnull(admits both no-prior-entry and matching-prior-entry once the helper-side compare-then-write has confirmed wrap agreement). Producer-kind monomorphism: a newBuildContext.carrierProducerRegistrymap, keyed by(carrierType, dataFieldName)coords, records the first mutation that registers aSingleRecordTableFieldat each coord; the second producer’s helper reads it to enrich the rejection diagnostic when wrap shapes disagree (mixing a DML mutation and an@servicemutation on the same carrier type rejects at classify time, naming both producer mutations regardless of registration order). The rejection routes through the standardUnclassifiedField+Rejection.structural+validateUnclassifiedFieldpath; no parallel validator-mirror walk is needed. Load-bearing pins: producer@LoadBearingClassifierCheck(key = "carrier-data-field.single-producer-kind")onFieldBuilder.registerDmlCarrierDataFieldpairs with@DependsOnClassifierCheckonFetcherEmitter.buildSingleRecordTableFetcherValue(the wrap-permit dispatch relies on at most one wrap shape reaching the emitter per coord); producer@LoadBearingClassifierCheck(key = "carrier-data-field.service-producer-strict-return")onFieldBuilder.registerServiceCarrierDataFieldis a hygiene-rejection check (no consumer annotation owed; the cast safety it backstops is already pinned bysource-key.result-row-walk-target-aligned-empty-pathvia theWrap.TableRecord(target.recordClass())invariant). Tests: unit-tierSourceKeyTest(six cases pinning the loosenedReader.ResultRowWalkcompact-constructor invariant:Wrap.RecordandWrap.TableRecord(target.recordClass())admit; cross-tableWrap.TableRecord(other)rejects with target-aligned message; non-empty path rejects under either admitted wrap;Wrap.Rowrejects); pipeline-tierSingleRecordTableFieldServiceProducerPipelineTest(eight cases: ONE/single-PK, MANY/single-PK, and MANY/composite-PK FilmActor admission pins theWrap.TableRecord(target.recordClass())shape and registeredSourceKey.columns; wrong-element-type,Set<XRecord>,Iterable<XRecord>reject through the strict-return predicate; mixed-producer DML-first and@service-first rejection asserts both producer mutation names and both wrap shapes appear in the diagnostic); execution-tierSingleRecordTableFieldServiceProducerExecutionTestagainst native PostgreSQL (five cases on SakilaFilmCarrierService/FilmActorCarrierServicefixtures: MANY-arm single-PK input-order preservation through the R141 PK-keyed-map walk; MANY-arm composite-PK input-pair-order preservation exercising the typedrow(pk1, pk2).in(…)predicate emission andList.of(r.get(pk1), r.get(pk2))map-key shape unique to multi-column keys; empty-source short-circuit; ONE-arm end-to-end; ONE-arm@servicereturns null → graphql-java does not traverse the carrier, payload rendersnullend-to-end);GraphitronSchemaBuilderTest’s `SINGLE_RECORD_CARRIER_DATA_FIELDrepurposes asORPHAN(Query-rooted carrier with no producing mutation: assert nofieldRegistryentry);SingleRecordCarrierPipelineTest.carrier_returnedFromQueryField*retargets to assert the orphan no-registration invariant. Out of scope (called out, not regressed): R141’s PK-keyed-map →VALUES-idx-JOINmigration (working code with its own audit surface; refactor of working code, not part of producer admission);Reader.ResultRowWalkconsumed outsideSingleRecordTableField(the widened invariant pairs only with the carrier data field’s permit today; any future consumer must adopt the same wrap-dispatch pattern or split its own permit);@serviceproducer withDataElement.Recorddata field (identity-passthrough permitSingleRecordIdentityFieldis producer-kind-irrelevant because the data field’s value IS the parent’s, regardless of producer). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R12 (carrier-walk LocalContext phases:
eb590efsplitErrorChannelinto sealedPayloadClass | LocalContext;c8731c9addTransportdiscriminator onChildField.ErrorsField;676ff72producer wiring forCarrierFieldRole.ErrorChannelRole;04799b8emitErrorRouter.dispatchToLocalContext;d7d1c55wire emit-time fork forErrorChannel.LocalContext;9cce63fselectErrorsField.Transportfrom parent’s resolved channel;a96766dregisterErrorsFieldon carrier-walk carriers +MappingsConstantNameDeduparms;093779cvalidator mirror for LocalContext errors-channel guard;f3ddcd4thread non-null sentinel through LocalContext catch path + pipeline / execute tests;93936d8anchor null-source guard sites to validator allow-list via audit annotations;4f1333frefresh spec body + split open execute-tier fixtures to R169 / R170): emit per-fetcher error channels from@error. Closes the long-standing "no generation (error mapping config)" gap so a payload’serrors: [SomeError!]field now routes a thrown exception into the typed payload instead of leaking the raw exception message through graphql-java’s defaultSimpleDataFetcherExceptionHandler. Foundational pieces landed first (sealedHandlertaxonomyExceptionHandler | SqlStateHandler | VendorCodeHandler | ValidationHandleronGraphitronType.ErrorType.Handlerwith parse-time lift and rules 1-6;ChildField.ErrorsFieldadmitting where the fivePolymorphicReturnTyperejection arms used to fire; theErrorChannelcarrier withmappedErrorTypes,payloadClass,errorsSlot,defaultedSlots,mappingsConstantName; classify-timeMappingsConstantNameDedupwith 8-hex SHA-256 collision suffix; channel-level rule 7 multi-VALIDATION + rule 8 duplicate-criteria checks;ErrorRouter.dispatch/redact/Mappingtaxonomy emitted at<outputPackage>.schema.ErrorRouterwithErrorMappingsconstants alongside;(List<String>, String)-ctor classifier check viaClass.forName; per-@error-union/interfaceTypeResolverregistration and per-@error-typepath/messagefield DataFetchers inGraphitronSchemaClassGenerator; source-direct dispatch with no developer@errorJava backing class;Optional<ErrorChannel>slot on everyWithErrorChannelpermit; per-fetcher try/catch wrapper +.exceptionallyasync tail routing the catch arm throughErrorRouter.dispatchorErrorRouter.redact; DML payload assembly + dispatch;ResultAssemblyfor service-side payload assembly; rule 6 relaxation + per-(channel, @error type, handler)source-class accessor reflection check via R88’sClassAccessorResolver;extensions.constraintfield population onConstraintViolations.toGraphQLError; child@service/@tableMethoderrorChannellift;@service/@tableMethoddeclared-exception channel-coverage check). The In Review pass shipped the carrier-walk LocalContext story (the R161 enabler):ErrorChannelis now a sealedPayloadClass | LocalContextinterface with the channel-agnosticmappedErrorTypes()/mappingsConstantName()accessors; theBuildContext.classifyCarrierFieldproducer admits errors-shaped wrappers asCarrierFieldRole.ErrorChannelRolewith aLocalContextbinding ahead ofDataChannelresolution;ChildField.ErrorsFieldgrows aTransport transport()component (PayloadAccessor | LocalContext) selected at classify time from the parent’s resolved channel;TypeFetcherGenerator.catchArmandasyncWrapTailswitch exhaustively on the sealed root and emitErrorRouter.dispatchToLocalContext(…)for the LocalContext arm. The runtime fix threads a typedP sentinelthroughdispatchToLocalContext: graphql-java’scompleteValueForObjectshort-circuits children on a null parent, so the catch arm now packsdata(sentinel).localContext(List.of(t)).build()wheresentinel = DSL.using(SQLDialect.DEFAULT).newRecord(<pk>)(single) /newResult(<pk>)(bulk); the data field’s null-source guard renders the SDL response asdata: nullwhile the errors field reads viaenv.getLocalContext(). ThecatchArmhelper carries a generator-internal 3-arg overload that throws when a LocalContext callsite forgets the sentinel. Load-bearing pin: producer@LoadBearingClassifierCheck(key = "error-channel.local-context-transport")onBuildContext.classifyCarrierFieldpairs with@DependsOnClassifierCheckonTypeFetcherGenerator.dispatchToLocalContextCatchArm, theTransport.LocalContextarm ofFetcherEmitter.dataFetcherValue,FieldBuilder.transportForParent, the validator mirrorGraphitronSchemaValidator.validateLocalContextErrorsFieldGuards, and the four per-variant emitter sites (buildSingleRecordTableFetcherValueRecordWrap/…TableRecordWrap,buildSingleRecordIdFromReturningFetcherValue,buildSingleRecordTableFromReturningFetcherValue,buildSingleRecordIdentityFetcherValue) that anchor eachif (source == null) return null;guard to the audit harness. The validator mirror rejects schemas whoseErrorsFieldcarriesTransport.LocalContextbut whose sibling data-channel field is outside theLOCAL_CONTEXT_GUARDED_DATA_CHANNEL_VARIANTSallow-list, turning a future widening that admits a non-guarded variant into a build-timeRejection.AuthorError.Structuralrather than a request-time NPE. Tests: unit-tierErrorRouterClassGeneratorTest(13 cases pinningdispatchToLocalContext(thrown, mappings, env, sentinel)signature + body packs sentinel intodata()); pipeline-tierSingleRecordCarrierPipelineTest(34 cases including the three R12 + R161 integration tests: single-inputMutationDmlRecordFieldand bulk-inputMutationBulkDmlRecordFieldeach classify witherrorChannel = Optional.of(LocalContext)and a siblingErrorsFieldwithTransport.LocalContext; emit pin forSQLDialect.DEFAULT/newRecordsentinel construction andenv.getLocalContext()reading); pipeline-tierErrorChannelClassificationTest(21 cases including carrier-walk LocalContext admission + rule 7 multi-VALIDATION rejection through the unified walk); validator-tierLocalContextErrorsFieldValidationTest(3 cases: guarded sibling passes, unguarded sibling rejects with allow-list diagnostic,PayloadAccessorErrorsField with unguarded sibling untouched); audit-tierLoadBearingGuaranteeAuditTestpicks up the producer + all consumers via the annotation scan; execute-tierGraphQLQueryTestadds Sakila SDL fixturesFilmCreateLocalContextPayload+FilmCreateConstraintViolation@errortype (handlerGENERICagainstorg.jooq.exception.IntegrityConstraintViolationException) with two end-to-end paths: validlanguageId=1round-trips with{film: {…}, errors: null};languageId=99999trips PostgreSQL FK 23503, routes throughdispatchToLocalContext, renders{film: null, errors: [{__typename: FilmCreateConstraintViolation, path: […], message: "…foreign key…"}]}. R2 retirement:checked-exceptions-typed-errors.md(Backlog R2) is subsumed by §4’s declared-checked-exception channel-coverage check and can be retired. Out of scope (called out, not regressed): execute-tier coverage for the@serviceResultAssembly.Assemblyarm (split to R169, not blocked); execute-tier coverage for the JakartaValidationHandlerchannel (split to R170, blocked on R94emit-input-records); subscription error paths; batch-loader per-key error handling; federation entity-resolver errors; instrumentation hooks; transaction rollback semantics; consumer-facingExceptionHandlingBuilderanalogue (auto-wiring is the goal, no top-level handler to install); customExecutionStrategyfor non-error reasons. Behaviour shifts vs legacy (documented in user-facing migration table):IllegalArgumentExceptionmessages are no longer automatically exposed to clients (schemas relying on the legacy auto-leak must declare{handler: GENERIC, className: "java.lang.IllegalArgumentException"});DATABASEhandlers now match anySQLExceptionin the cause chain, not only those wrapped in Spring’sDataAccessException(non-Spring apps no longer needspring-jdbcfor database error mapping). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R161 (
d0f676cretireDmlReturnExpression.Payloadand unify@record-returning DML on the carrier-walk path;8a9a707lift theNoBacking/ClassBackedfork intoSingleRecordCarrierResolution.Ok): collapses the two parallel@record-returning DML designs onto Path 1.BuildContext.tryResolveSingleRecordCarrier’s candidate predicate widens from `{PlainObjectType, PojoResultType.NoBacking}to{PlainObjectType, ResultType}so every@record(record:{className:})wrapper (Backed,JavaRecordType,JooqRecordType,JooqTableRecordType) routes throughMutationDmlRecordField/MutationBulkDmlRecordFielduniformly;Mutation*TableFieldpermits are now guaranteed never to carry a@recordreturn, enforced structurally rather than via classifier-acceptance shape. TheDmlReturnExpressionsealed type collapses to four arms (EncodedSingle/EncodedList/ProjectedSingle/ProjectedList); thePayloadAssembly/RowSlotmodel types, the reflection-based resolver (resolveDmlPayloadAssembly,buildDmlPayloadAssemblyBeanArm,DmlPayloadAssemblyResult,NO_ASSEMBLY), and the emit layer (emitPayload,emitPayloadCtor,emitPayloadSetters) all retire.MutationInputResolver.validateReturnTypedrops thefqClassName == nullguard on theResultReturnTypearm so the carrier-walk probe runs unconditionally ; one probe over the SDL shape, not two probes composing. The follow-up commit lifts the consumer-sideparentType instanceof PojoResultType.NoBackingre-narrowing atGraphitronSchemaBuilder:227into the model as a sealedOkinterface withNoBacking/ClassBackedrecord sub-arms (BuildContext.tryResolveSingleRecordCarriertags the outcome viatarget instanceof ResultType && !(NoBacking)), per Generation-thinking: type-level classification short-circuits to carrier-walk registration only onNoBacking;ClassBackedfalls through to normal per-type classification so R88’s per-field accessor-resolution diagnostics surface on developer-supplied classes, and the mutation classifier reclassifies the data field via compare-then-write at mutation time.code-generation-triggers.adoc’s Mutation Fields trigger table is qualified ("returning ID or a `@tabletype" on the fourMutation*TableFieldrows) and gains two new rows forMutationDmlRecordField/MutationBulkDmlRecordField; the.returningResult(pkCols)design decision is captured onbuildMutationDmlRecordFetcherandbuildMutationBulkDmlRecordFetcher(PK-only RETURNING keeps the write transaction minimal; data-field projection runs in a separate read-only follow-up SELECT outside the transaction).LoadBearingClassifierCheckdescriptions onresolvePayloadConstructionShapeandbuildDmlFieldare trimmed to reflect the post-R161 consumer set (DML-rowemitPayloadremoved; four-armDmlReturnExpression). Tests: pipeline-tierDML_RECORD_PAYLOAD_RETURN_HAPPYandDML_RECORD_PAYLOAD_ROW_ONLY_HAPPYre-target as carrier-walk admission tests (DML_RECORD_CARRIER_WITH_ERRORS_HAPPY/DML_RECORD_CARRIER_ROW_ONLY_HAPPY) exercising theJavaRecordTypewrapper arm admitted by the R161 widening;DML_RECORD_PAYLOAD_NO_ROW_SLOT_REJECTEDrepurposes asDML_RECORD_CARRIER_NO_DATA_CHANNEL_REJECTED(the carrier walk rejects the SDL shape for missing aDataChannelfield rather than reflecting on the developer’s class);DML_RECORD_PAYLOAD_LIST_REJECTEDkeeps its name with the rejection diagnostic now coming fromvalidateReturnTypeinstead ofresolveDmlPayloadAssembly.FetcherPipelineTest’s `dmlMutation_setterShapePayload_emitsSetterFactory,dmlDeleteField_recordPayloadReturn_successArmConstructsPayloadAndCatchArmDispatches,dmlDeleteField_recordPayloadReturnNoErrorsField_successArmConstructsPayloadCatchArmRedacts, and the unusedSetterShapeDeleteFilmPayloadfixture all delete (the bodies they asserted on no longer exist). After migration: zero references toDmlReturnExpression.Payload,PayloadAssembly,RowSlot,emitPayload*, orresolveDmlPayloadAssemblyanywhere in the codebase. Out of scope (called out, not regressed): execution-tier coverage for the fourResultTypeclassName-carrying arms (sakila’sFilmPayloadalready pins the carrier-walk emit shape viaNoBacking, and post-R161 every wrapper state runs the same emitter code, so a bespoke fixture would assert structural rather than behavioral coverage); consolidatingMutationFieldpermits under verb-on-permit-identity (MutationInsertResultField/ etc., tracked at R162). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R39 (
f8fc604wip validator + tests;d0cf6b5validator ship + carrier permit lift;2600955lift PK-derived orderBy toOrderingOwnedByProducersealed marker): Cross-cutting validator rejectsSqlGeneratingField+FieldWrapper.List+OrderBySpec.Noneat build time, closing the silent-non-determinism gap on list fields targeting no-PK tables (OrderByResolver.resolveDefaultOrderSpecfalls back toOrderBySpec.Nonewhen no@defaultOrder/@orderByis present and the target has no PK, which generators faithfully emit asList.of(); noORDER BY; producing visibly different row order each run). Three checks now cover three disjoint shapes:validatePaginationRequiresOrdering(paginated, including connections),validateSplitTableFieldconnection branch (@splitQuery connections),validateListRequiresOrdering(plain[T]list wrapper). Gated onFieldWrapper.Listnotwrapper().isList()so the three messages stay non-overlapping; the sealedFieldWrapperpermit list (Single/List/Connection) is the typed gate. Marker carrier: a new sealedOrderingOwnedByProducer(model package) permitsChildField.SingleRecordTableFieldandChildField.ServiceTableField; the validator excludes marker-bearing permits by type. The carrier permit’s structurally-emptyorderBy() = Nonestays ; the visible result order on these permits is owned by an upstream producer (FetcherEmitter’s PK-keyed-map walk for the R141/R158 carrier; the developer’s `@servicemethod forServiceTableField), not by the field’s ownorderBy()component. An earlier shape derived a PK-fixedOrderBySpec.FixedfromsourceKey.columns()insideSingleRecordTableField.orderBy()to side-step the validator; principles-architect flagged this as duplicatingOrderByResolver.resolveDefaultOrderSpec’s no-directive branch and coupling the validator’s correctness to a `FieldBuilderclassifier guarantee that no involved file named. The marker refactor moves the exemption into the type system: find-usages from either permit lands on the validator’s exclusion site, and adding a new permit to the sealed marker is the explicit deliberation point. The check is hygiene-rejection (no emitter relies on it;TypeFetcherGenerator.buildOrderByCode/buildConnectionOrderingBlock/buildBaseReturnExprandInlineTableFieldEmitterdefensively handleNone/emptyFixedforSingleandConnection-without-pagination shapes the validator does not gate), so no@LoadBearingClassifierCheck/@DependsOnClassifierCheckpair is owed. Tests: unit-tierListRequiresOrderingValidationTest(5 cases pinning Query-rooted + child-position dispatch path × list/single × ordered/unordered); pipeline-tierValidateListRequiresOrderingPipelineTest(reject + admit through SDL → classified model →GraphitronSchemaon the Sakila no-PKfilm_listfixture, asserting the contract’d error message); five pre-existing tests ({Lookup,QueryLookup,RecordTable,RecordLookupTable}FieldValidationTest) updated to use PK-fixed orderBy where they incidentally usedOrderBySpec.None+FieldWrapper.List; in every case the test’s subject is FK paths / projection / cardinality, not ordering. Marker-exemption coverage rides on the existing R141 / R158 execution-tier sakila tests (FilmsPayload,FilmsServicePayload,FilmActorsServicePayload): each declares a list-shaped data field with no@defaultOrder; the validator must admit them or the entire suite fails to build. Out of scope (called out, not regressed): requiring ordering on single-value fields (no-op); changingOrderByResolverto refuseOrderBySpec.None(the validator is the right layer for "legal in the model but illegal as authored schema"); merging the cross-cutting checks (remediation text deliberately differs). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R165 (
28cea70gate empty fetcher-registration bodies at construction):FetcherRegistrationsEmitter.emitwas producing empty-body entries for unreferenced payload-shaped types (the field-report reproducer was an unreferencedSlettRegelverksamlingPayload { regelverksamlingId: [ID!] @nodeId }), which surfaced as ajavacerror in consumer projects:GraphitronSchema.build()emitted<Name>Type.registerFetchers(codeRegistry)for every key in the bodies map, whileObjectTypeGeneratorskipped the method emission when the body was empty. Fix gates emptiness at the construction site rather than via a post-pass scrub:typeBodyandnestedBodyreturnOptional<CodeBlock>(empty when the classified-field list is empty), and the twoputcall sites inemituse.ifPresent(body → result.put(name, body)). The deadfields.isEmpty()short-circuit insidebuildBodyis removed (typeBody guards before calling).ObjectTypeGenerator’s `fetcherBody != null && !fetcherBody.isEmpty()gate collapses to a null check (the!isEmpty()half is dead under the new invariant; the null half still guards types absent from the keyset). Load-bearing pair under keyfetcher-registrations.no-empty-bodiespins the producer-side guarantee:@LoadBearingClassifierCheckonFetcherRegistrationsEmitter.emit, matching@DependsOnClassifierCheckonGraphitronSchemaClassGenerator.generate’s keyset iteration; `LoadBearingGuaranteeAuditTestcatches future drift as an orphaned-consumer audit failure rather than a downstreamjavacerror. Tests: unit-tierFetcherRegistrationsEmitterTest(post-condition thatemit’s returned map has no empty `CodeBlockvalues, plus key-absence on the bug-reproducing payload fixture, across single-record carrier and connection/edge code paths); pipeline-tierFetcherRegistrationsPipelineTestasserts the bi-directional set-equality invariant between<Name>Type.registerFetchers(codeRegistry)call sites in the emittedGraphitronSchema.build()body and the type names whoseObjectTypeGeneratorTypeSpecdeclares aregisterFetchersmethod, on both the field-report fixture and a realistic mixed fixture (pinning both directions catches drift whichever side of the keyset/method contract moves). Out of scope (called out, not regressed): reachability-based pruning of unreferenced SDL types (the orphan payload survives viaadditionalTypes); strengtheningemit’s return type to a `FetcherBodiesrecord orMap<String, NonEmpty<CodeBlock>>carrier (R166 floats the broaderGraphQLSchemaVisitor-driven emission rework where this would land). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R154 (Phase 1 model lift + Phase 2 setter-shape admission, both landed before history-squash; landing SHAs unavailable post-squash): Admit a second
@servicepayload construction shape ; public no-arg constructor + per-SDL-field Java-bean setters ; alongside today’s canonical all-fields constructor. New sealedPayloadConstructionShape { AllFieldsCtor | MutableBean }carries the contract;MutableBean.bindingsholds oneSetterBinding(sdlFieldName, setter, acceptsOptional)per SDL field in declaration order. The three carrier slot types lift in lockstep:ErrorsSlot,ResultSlot,RowSloteach become sealed withCtorParameterIndex(int)+SetterMethod(boundSetter, List<NonBoundSetter>)permits (kept as three sibling hierarchies, not folded onto one broadSlotinterface, soErrorChannel/ResultAssembly/PayloadAssemblykeep role-specific access withoutinstanceofwidening). NewNonBoundSetter(setter, defaultLiteral)record carries each non-bound SDL field’s setter paired with its language-default literal, so the catch-arm payload-factory emit walks one structured list and prints each setter call with its default value. Classifier:FieldBuilder.resolvePayloadConstructionShape(payloadCls, sdlFieldNames)returns a sealedPayloadConstructionShapeResult { Resolved(shape) | Reject(reason) }; predicates run in order withAllFieldsCtorfirst (canonical-over-bridge precedence: records always present the all-fields ctor; the setter shape is a legacy bridge fromgraphitron-codegen-parent); a class supporting both shapes resolves toAllFieldsCtor; the only rejection mode is neither-predicate-matches, with structured guidance enumerating the three escape hatches (convert to record / remove extra ctors / add no-arg + Java-bean setters). Three resolvers (resolveErrorChannel,resolveServiceResultAssembly,resolveDmlPayloadAssembly) consume the sealed shape and split into ctor-arm + bean-arm builders. Emit:TypeFetcherGenerator’s three payload-factory sites (catch-arm `errors → …lambda, service-result success arm, DML-row success arm) dispatch onPayloadConstructionShapevia exhaustive sealed switch; the bean arm emitsvar p = new Payload(); p.setBound(…); p.setOther(<default>); …; return p;instead ofnew Payload(…). Load-bearing pins: producer-side@LoadBearingClassifierCheckannotations onresolvePayloadConstructionShapefor the keyspayload-construction.shape-resolved(carrier-arm-totality) andpayload-construction.setter-name-matches-sdl-field(setter.getName() is callable into the generated source); consumer-side@DependsOnClassifierCheckon the three emit-site forks plus the carrier helpers. Tests: unit-tierPayloadConstructionShapeTest(7 cases pinning record→AllFieldsCtor, bean→MutableBean, both-shapes→AllFieldsCtor canonical wins, missing-setter rejection naming the offending field, multi-ctor-no-no-arg→Reject,Optional<T>setter sets acceptsOptional, camelCase SDL field resolves through Java-bean namingxRating → setXRating); pipeline-tierFetcherPipelineTestfour R154 cases (serviceMutation_setterShapePayload_emitsSetterFactory,_allFieldsCtorPayload_emitsCtorFactory_unchangedregression,_bothShapesPresent_prefersCtorFactory,dmlMutation_setterShapePayload_emitsSetterFactory); compilation-tiergraphitron-sakila-serviceaddsSetterShapeFilmReviewPayload(no-arg ctor +setReviewId/setErrors) plusFilmReviewService.submitSetterShapereturning that type; sakila-example schema addssubmitSetterShapeFilmReviewmutation; execution-tierGraphQLQueryTesttwo end-to-end cases against real PostgreSQL (submitSetterShapeFilmReview_validInput_returnsHappyPathPayloadand_invalidRating_routesThroughBadRatingErrorType) round-trip both the success-arm and the error-arm catch-arm payload-factory. Out of scope (called out, not regressed): builder-pattern (fluent immutable) payload classes (Payload.builder()….build()) ; a separateBuilderPatternpermit onPayloadConstructionShapeif a real schema surfaces it; replacingAllFieldsCtorwithMutableBean(the two coexist; records remain the recommended shape); designing a@constructionShape(setter)SDL directive to disambiguate (per configuration drift reasoning, the structural signal the classifier already sees is sufficient); a deprecation diagnostic when a bean-shape payload could be a record (the parallel-support window has no deprecation pressure). Minor housekeeping noted at In Review → Done:SetterBinding.acceptsOptionalis captured by the predicate but not consumed by any emit site (anOptional<T>setter receivesnullrather thanOptional.empty()for default-slot inserts); the spec’s "parameter-type mismatch rejection" unit case is unimplemented (tryMutableBeanaccepts any single-arg setter on name match alone, the legacy convention’s de-facto behaviour). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R156 (
ba4697fPhase A model:DataElement.Id,PerFieldOutcome,PkResolution, two newChildFieldsiblings;cbe4634Phase B verb-aware carrier walk + DELETE projection;61ce2c8Phase C/DFieldBuilderrewire +MutationFieldDELETE admission lift +FieldRegistry.reclassify;8d88bb5Phase E/F per-field emitters + DELETE DML chain emission;fe676bfPhase G partial L1/L3;a869716Phase G L5 sakila fixtures;160f102Phase G L6 execution-tier coverage;e08c439Phase H user docs;424bc42doc sweep;b5209a9In Review rework ;PkResolutionEmitterReachabilityTest,MutationDmlNodeIdClassificationTestadmission cells,MutationDeletePayloadCarrierCaserejection rows):@mutation(typeName: DELETE)now supports payload-returning carriers on two element arms, closing the gap that left DELETE the only DML verb without a payload-carrier story (a prerequisite for composing with R12’serrors:channel). NewDataElement.Id(name, wrapper)arm admits anID/[ID!]carrier field that echoes encoded NodeIds of deleted rows; admitted only on DELETE per the permit-verb rule (PK-echo commits to the PK as the entire post-image; INSERT/UPDATE/UPSERT post-images are richer). The existingDataElement.Tablearm narrows on DELETE: a new builder-internalPerFieldOutcome(five arms:PkRead,NonPkNullable,NonPkNonNullable,ServiceField,UnsupportedField) classifies every field on the element SDL type, andBuildContext.classifyDeleteTableProjectioneither rejects (any non-PK-non-null /@service/ FK-traversing / unsupported leaf, with a diagnostic naming the offending field) or projects to the narrow model-facingPkResolution(two arms) carried on a newChildField.SingleRecordTableFieldFromReturningpermit. The companionChildField.SingleRecordIdFieldFromReturningcarries theCallSiteCompaction.NodeIdEncodeKeysfor theIdarm; both are siblings of the existingSingleRecordTableFieldand load-bearing for distinct invariants (no follow-up SELECT after DELETE, the row is gone).BuildContextgains a verb-awaretryResolveSingleRecordCarrier(typeName, DmlKind)overload that delegates to the verbless walk and layers DELETE-admissibility on top, so the unconditional DELETE-rejection atFieldBuilder.java:2960-2965disappears entirely;MutationDmlRecordField/MutationBulkDmlRecordFieldcompact constructors lift the DELETE rejection symmetrically.FieldRegistry.reclassifyis the named exception that lets the DELETE carrier path replace the verbless walk’sSingleRecordTableFieldregistration (which assumed follow-up SELECT) with the DELETE-specific sibling.FetcherEmitteradds two methods:buildSingleRecordIdFromReturningFetcherValuereads PK column(s) off the sourceRecordand runs them through the encoder;buildSingleRecordTableFromReturningFetcherValuesynthesizes a PK-onlyRecordviaTables.<TABLE>.newRecord()and copies PK columns from the RETURNING source (the same-Field<T>-instance round-trip is the load-bearing assumption documented on the emitter and in the spec’s §Runtime caveats). The producer-consumer pin is@LoadBearingClassifierCheck(key = "mutation-delete-carrier.pk-resolution-projection-clean")onclassifyDeleteTableProjectionplus matching@DependsOnClassifierCheckon the table-arm emitter. Tests: unit-tierDataElementIdInvariantTest(6 cases pinning compact-constructor wrapper invariants, singleton ID/ID! and [ID!]/[ID!]! admit, list-of-nullable and Connection wrappers reject); unit-tierPkResolutionEmitterReachabilityTest(4 cases: reflective scan ofPkResolutionarms againstFetcherEmitter’s `HANDLED_BY_EMITTERallowlist,PerFieldOutcomerejection arms exist and do NOT leak intoPkResolution, record-component symmetry across the two sealed roots,@LoadBearingClassifierCheckpin reflectively confirmed onclassifyDeleteTableProjection); pipeline-tierMutationDmlNodeIdClassificationTestgains six R156 rows (single/bulk × implicit/explicit@nodeIdadmission cells overnodeidfixture’s composite-PK `Barand single-PKBaz, plus wrong-encoder-table and no-@node-backed-input-table rejection paths); pipeline-tierMutationDeletePayloadCarrierCaseparameterised inGraphitronSchemaBuilderTestcovers the admission/rejection matrix (nullable non-PK admits withPkResolution.NonPkNullableprojection; non-null non-PK rejects naming the field and pointing atDataElement.Id; INSERT/UPDATE/UPSERT +[ID!]reject via permit-verb rule;[ID]list-of-nullable rejects at the verbless walk;@service-resolved element field rejects); compile-tiergraphitron-sakila-exampleaddsDeletedFilmsIdPayload,DeletedFilmsTablePayload,DeletedFilmInfotypes +deleteFilmsIdCarrier/deleteFilmsTableCarriermutations; execution-tierDmlBulkMutationsExecutionTestadds two end-to-end tests against real PostgreSQL proving the encoded-NodeIds list comes back in input order and the per-field PK projection through the synthesized Record resolves. Deviation from spec (called out in spec body): user docs landed atdocs/manual/reference/directives/mutation.adocrather thandocs/manual/reference/mutations.adocbecause the existing layout puts directive references underdirectives/. Out of scope (called out, not regressed): affected-row count payload field (separate Backlog item; structurally different role permit); error-channel composition (R12’s upstream producer;ErrorChannelRolepermit already composes with the new arms);RETURNING *or projection-aware RETURNING for arbitrary non-PK columns (rejected in §Alternatives, the user’s rule narrows projections to PK only); dialect-capability gating on DELETE-RETURNING (existing dialect-roadmap item covers RETURNING capability checks); soft warnings on silent-null non-PK nullable fields (classifier stays binary; documented behaviour instead). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R159 (
6aababeadmit + type-match + LSP arms;8671b5bthread sourceSigil into DataChannel, route LSP through siteContext, drop back-compat constructors): root-value sigil$sourceon@field(name:)for carrier-payload sourcing. Authors now have an explicit, name-decoupled way to confirm the implicit binding between a@service-backed mutation’s reflected return and the SDL carrier-payload data field (the R158 contract). NewFieldSourceSigilutility owns the sealedFieldNameRef = BareName | UpstreamRoot,ParseResult = Absent | Ok | UnknownSigil, andSiteContext = CarrierDataField | Other; the three canonical messages (unknown sigil, not-defined-here, type mismatch) live on the utility so classifierHardReject, LSPDiagnostics, and LSPFieldCompletionsroute through one source.BuildContext.classifyCarrierFieldinterposesparseArgFieldNameRefbefore the forbidden-directive loop:UpstreamRootlifts@fieldoff the forbidden list for the iteration and threadssourceSigil = trueinto the emittedCarrierFieldRole.DataChannel(fieldName, element, sourceSigil);UnknownSigilHardRejects before the forbidden-directive loop fires (so the author sees "Unknown sigil" rather than "forbidden directive"). The type-match check runs atFieldBuilder.classifyMutationField’s `@service Resolved.Resultarm (the colocation principle is preserved by the sharedsourceSigilTypeMatchescallable, with the bit-read replacing the SDL re-parse). LSP plumbing:CatalogBuilder.projectCarrierDataFieldswalksGraphitronSchema.fields()forChildField.SingleRecord*permits to projectMap<String, String> carrierDataFieldByTypeontoLspSchemaSnapshot.Built;Built.siteContext(typeName, fieldName) → FieldSourceSigil.SiteContextis the one entry point consumers use, so broadening admit in a future item flips a single sealed return-value.FieldCompletionsadmits$sourceat carrier-data-field sites and stays silent everywhere else (including snapshot-uncertainty: no entry in the carrier projection → no suggestion).Diagnostics.validateFieldMemberemitssourceSigilNotDefinedHereMessageat non-carrier sites whose parent’sTypeBackingShapeis known; snapshot-uncertainty stays silent (defers to the build).LspSchemaSnapshot.Built.Current/Built.Previouscollapse to single canonical three-arg constructors; the two-arg back-compat overloads were a shim with no external producer to protect. Tests: pipeline-tierFieldSourceSigilPipelineTestcovers admit, model-shape regression (with/without@fieldbyte-identical), type-mismatch reject, unknown-sigil reject (parse-time arm fires before forbidden-directiveHardReject), bare-name regression, non-carrier-site regression (today’s accessor-mismatch unchanged) ; each rejection case also asserts validator-surfaceValidationReport.errors()via the same fixture; LSP-tierFieldCompletionsTest+DiagnosticsTesteach gain three R159 cases (admitted / non-carrier / snapshot-uncertainty). Deviation from spec (called out in spec body): type-match site isFieldBuilder.classifyMutationField, notclassifyCarrierField, because the producer’sMethodRefis bound at consumer-site classification not at the carrier walk;FieldSourceSigil.sourceSigilTypeMatchesuses exact equality for bothDataElement.TableandDataElement.Recordtoday (spec called for assignability on the@recordarm; the implementer’s note acknowledges "future items may relax when a forcing function appears"). Out of scope (called out, not regressed): admission at sites other than the carrier-payload data field (future broadening flipssourceSigilDefinedAt);$errors/$context/ other sigils; dotted paths in@field(name:); multi-step path-expression grammar; DML-producer carrier walk migration to a$sourcemodel (R75 / R141 keep PK-keyed-map); execution-tier coverage of the OpprettRegelverksamlingPayload-shaped end-to-end gated on R158 landing the consumer-side fetcher. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R139 (dev-pipeline to LSP schema-snapshot side-channel; first client unknown-directive validator): the bundled
directives.graphqlswas the LSP’s entire view of "what directives exist", so any user-authored directive (@key(fields:),@requiresAuthentication,@auth(role:), etc.) drew aDiagnosticswarning per use as if it were a typo. Fix: a secondvolatileref onWorkspace(alongsidecatalog) carrying a projection of the parsed user schema, populated from the same parse the catalog already runs. New sealedLspSchemaSnapshot(Unavailable | Built.{Current,Previous}) over two orthogonal axes ; availability (built yet?) and freshness (latest successful parse?) ; lifts theDiagnostics.validateClassName-style "pre-build silence" gate from a single arm into a typed sub-hierarchy.BuiltcarriesList<DirectiveShape>with siblingInputValueShape+ sealedTypeShape(Named | List) so phase-2 arg-validation consumers discriminate list-vs-named without re-parsing rendered SDL. Producer isCatalogBuilder.buildSnapshot(TypeDefinitionRegistry)(returnsBuilt.Currentonly; failures throw upstream inGraphQLRewriteGenerator), pinned with@LoadBearingClassifierCheckon the keyssnapshot-built-implies-clean-parseandsnapshot-directive-roundtrip-faithful. Atomic-pair swap path isWorkspace.setBuildOutput(…)(absorbing the spec’s separately draftedsetCatalogAndSnapshotandsetCatalog-on-classpath setters, since the validator-report side-channel that landed on top of R139 wants the same atomic swap discipline); failure path isWorkspace.demoteSnapshot()which transitionsBuilt.Current → Built.Previousand is no-op onUnavailable/Previous. Resolution goes through sealedDirectiveResolution.resolve(LspVocabulary, LspSchemaSnapshot, String)returningBundled | User | Unknown, encoding bundled-shadows-snapshot precedence once so consumers never re-check it inline;Workspace.resolveDirective(String)wraps the static entrypoint for request callbacks that already hold aWorkspace. First client: the unknown-directive arm inDiagnostics.computeswitches exhaustively on the snapshot variant for the freshness-aware silence policy ; warns only underBuilt.Current + Unknown, silencesUnavailable(pre-build),Built.Previous(stale after parse failure), and anyUser/Bundledresolution.SPEC_BUILTIN_DIRECTIVES(skip,include,deprecated,specifiedBy,oneOf) keeps its short-circuit because graphql-java ships them implicitly.DevMojo.regenerate/rebuildCatalogboth callsetBuildOutput(…)on success anddemoteSnapshot()+markAllForRecalculation()on the parse-failure catch. Tests: unit-tierLspSchemaSnapshotTest(case-sensitive lookup across bothCurrent/Previous, unmodifiable defensive copy at construction) andCatalogBuilderSnapshotTest(directive round-trip, list/non-null sealed projection, no producer-side bundled-name filter, description round-trip); pipeline-tierDiagnosticsTestgrowsunknownDirectiveSilencedByUnavailableSnapshot,unknownDirectiveSilencedByStaleSnapshot,userDeclaredDirectiveSilencedBySnapshot,userDeclaredDirectiveShadowedByBundledStillValidates, plus the existingunknownDirectiveProducesWarningupdated to pass an explicitBuilt.Current(List.of(), Map.of()); compilation-tier sakila fixture declaresdirective @auth(role: String!) on FIELD_DEFINITIONand applies it onQuery.customersas the input-contract regression guard. Out of scope (called out, not regressed): hover / arg-completion / arg validation against user directives (later items; phase-2 armvalidateUnknownArgsAgainstSnapshot/validateRequiredArgsAgainstSnapshotrides on the same plumbing); adeclaredTypeNamesset onBuilt(R157 widened the permits withtypesByNamefor the analogous record/POJO use case); wideningLoadBearingGuaranteeAuditTestacross the graphitron / graphitron-lsp module boundary (consumer-side@DependsOnClassifierCheckmarkers onDiagnostics.computeandWorkspace.resolveDirectiveare find-usages-only by design); a shadow-warning for user directives that redeclare bundled names; server-mode LSP without the dev mojo. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R157 (
6c33331implement LSP@field(name:)coverage for@record-bound types;6b9ea86sealed-splitJooqRecordBacking, drop back-compat shims, rename validator):@field(name: "X")autocomplete / diagnostics / hover only fired on jOOQ-table-bound parents; under@record-declared Java records, POJOs, and standalone jOOQ records the three consumer sites (FieldCompletions.generate,Diagnostics.validateCatalogColumn,Hovers.columnHover) silently returned empty because they parsed the enclosing type’s@tabledirective off the SDL AST and had no path to consult the classifier’s record/POJO knowledge. Fix: shift the LSP off SDL re-sniffing onto the classifier’s lifted model. New sealedTypeBackingShape(RecordBacking | PojoBacking | JooqRecordBacking{WithTable|Standalone} | TableBacking | NoBacking{Root|UnbackedResult|UnclassifiedInterface}) projects everyGraphitronTypepermit to the LSP-visible backing shape; the projector lives inCatalogBuilder.projectTypeas an exhaustive sealed switch so a futureGraphitronTypevariant trips a compile error at the projection site.LspSchemaSnapshot.Built.{Current,Previous}broaden to carryMap<String, TypeBackingShape> typesByNamealongside the R139directiveslist;GraphQLRewriteGenerator.buildOutputwires the three-argbuildSnapshot(registry, schema, catalog)form.ClasspathScannerreads the JVMRecordattribute viajava.lang.classfile.attribute.RecordAttributesoCompletionData.ExternalReferencecarriesList<RecordComponent>per scanned class; the projector consumes these forRecordBackingand the bean-accessor filter (get<X>/is<X>no-arg public method) lives inCatalogBuilder.beanAccessorSlotforPojoBacking. The three consumer sites pattern-dispatch on the sealed permit (Diagnostics.validateCatalogColumnrenamed tovalidateFieldMember, since it dispatches across four backing shapes plusNoBackingnow);TypeContext.tableNameOfis gone from the three sites but stays for@nodeId(typeName:)’s metadata projection (R152 owns that migration). The `@LoadBearingClassifierCheck("java-record-type-backs-record-class")annotation onCatalogBuilder.buildSnapshotpins the assumption the three LSP-side@DependsOnClassifierCheckconsumers make aboutJavaRecord{Input,}Typebacking real Java record classes; the audit-test scope isgraphitron-module-only by design (the LSP-side consumers wear the annotation for find-usages navigation and reviewer-signal purposes perrewrite-design-principles.adoc § "Classifier guarantees shape emitter assumptions"’s producer-without-consumer allowance). Tests: primary-tier `R157PipelineTestparses a realistic.graphqls, runsGraphitronSchemaBuilderfor real, scans the LSP module’starget/test-classesfor fixture classes (R157FilmRecord,R157FilmPojo), builds the full snapshot throughCatalogBuilder, and drivesFieldCompletions/Diagnosticsend-to-end; unit-tierCatalogBuilderSnapshotTestadds per-variant projection cases (TableType, TableInterfaceType, JavaRecordType, PojoResultType.Backed, JooqTableRecordType, JooqRecordType-standalone, RootType, plain InterfaceType, PojoResultType.NoBacking);ClasspathScannerTestcovers the Record-attribute read plus the plain-class empty case;FieldCompletionsTest,DiagnosticsTest,HoversTestadopt the snapshot-keyed dispatch and add positive cases perTypeBackingShapearm (RecordBacking → component list, PojoBacking → bean accessors, JooqRecordBacking.WithTable → column-on-table path, Standalone → silence, TableBacking → unchanged column-on-table path, NoBacking → silence). Out of scope (called out, not regressed):@enum(enum: {className:})types (don’t carry@field(name:));@reference(key:)on non-table backings (FKs are intrinsically a jOOQ-table concept; the directive stays on the existingTypeContext.tableNameOfpath); union types as@fieldparents (meaningless directly; flows toNoBacking.UnbackedResult); migrating@nodeId(typeName:)’s metadata projection onto `typesByName(R152 owns the table-of-other-type scoping bug); per-component nullability / Jakarta-constraint surfacing onMemberSlot(R12-adjacent). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R153 (
555fa0f+32c7ebe): Attach explicitTextEditrange to every LSPCompletionItem. Hoists the cursor walk + replace-range computation intoLspVocabulary.locateAt(returnsOptional<CursorLocation>carrying the schema coordinate plus the tree-sitter leaf node) andCompletionContext(carries the LSPRangederived by slicing the leaf:string_valuestrips one or three bytes per side, discriminated by content;enum_value/ barenameuse the full span);coordinateAtcollapses to a thin wrapper preserving theHoverscaller. All six string-value providers (ClassName,Method,Table,Field,Reference,ScalarType) plusNodeTypeCompletionsandArgNameCompletionsshipsetTextEdit, so eglot’s graphql-mode syntax table (which excludes.as a symbol constituent) no longer concatenates the prefix with the candidate on dotted FQNs likecom.example.FilmServ|becomingcom.example.com.example.FilmService. Empty literals and block strings flow through the same slicing rule (""and""""""collapse to zero-width at the inner cursor);ArgNameCompletionsalso fires on the arg-key side of an already-filled arg for partial-identifier completion. Coverage:LspVocabularyLocateAtTest(9 cases pinning node-kind dispatch plus empty-literal / block-string / empty-object-value corners) andCompletionTextEditTest(12 cases, one regression pin per provider plus cursor-on-quote / block-string / empty-literal / zero-width-on-whitespace). Out of scope (called out, not regressed):filterTextfor partial-match scoring andinsertTextsnippet syntax; the user-directive arg-name path does not yet fire on the arg-key side of an already-filled arg (the bundled arm does; incidental asymmetry, would warrant its own roadmap item if a gap surfaces). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R155 (
0bd77deboundary box + invariant pin;fc55fa6emitter-tier pin):graphitron:devblew up withIllegalArgumentException: couldn’t make a guess for inton consumer schemas whose input beans carried a Java-primitive field (recordint n, JavaBeanvoid setActive(boolean), …) becausejava.lang.reflect.Type.getTypeName()returns the unboxed primitive literal and that string flowed unchanged throughFieldBinding.javaElementTypeNameinto the twoClassName.bestGuesssites inInputBeanInstantiationEmitter(fieldLocalType,directExpr). Fix: normalise at the resolver boundary. NewInputBeanResolver.boxPrimitive(String)maps each of the 8 primitive literals (int/long/boolean/double/float/short/byte/char) to its wrapper FQN (java.lang.Integer/…) and passes everything else through;peelJavaListSetcalls it on the scalar return soFieldBinding.javaElementTypeNamebecomes invariantly a real class name, never a Java primitive literal. The list branches deliberately don’t box: Java disallowsList<int>, so the generic argument is always already a reference type. Javadoc on the helper and a sentence onFieldBinding.javaElementTypeNamepin the contract as prose; no@LoadBearingClassifierCheckannotations owed (this is representation-normalisation at the resolver, not a new classifier branch with downstream shape obligations). Tests: unit-tierInputBeanResolverBoxPrimitiveTestpins the full 8-arm primitive→wrapper mapping plus class-name pass-through plusint[]fallthrough; pipeline-tierGraphitronSchemaBuilderTestgains two cases (SERVICE_MUTATION_FIELD_INPUT_BEAN_PRIMITIVE_RECORD,SERVICE_MUTATION_FIELD_INPUT_JAVABEAN_PRIMITIVE_BOOLEAN) assertingFieldBinding.javaElementTypeName == "java.lang.Integer"/"java.lang.Boolean"on the record-component and JavaBean-setter paths respectively;TypeFetcherGeneratorTestgains two mirror cases pinning thatInputBeanInstantiationEmitter.buildSingularHelperno longer throws on a boxed primitiveFieldBindingand emits the wrapper-typed local and cast. Out of scope (called out, not regressed): generalisingClassName.bestGuessingraphitron-javapoetto accept primitives (javapoet treats primitives viaTypeName.INT, a wider refactor of no immediate value); tightening theString-typed representation ofFieldBinding.javaElementTypeName/EnumValueOf.enumClassNameto a typedClassNameor sealedJavaTypeRefso the "real class name, never a primitive literal" invariant is a type fact rather than prose (separate Backlog item). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R144 (
db40644ship cardinality safety default;a142f78cover@value+@conditionmutual-exclusion rejection;11bd6eedrop unreachableadmissibleCount == 0branches): Flip the polarity of mutation-input safety. Every input field on a DELETE / UPDATE@tableinput is a WHERE filter by default; the classifier enforces that the contributed filter columns cover the input@table’s primary key. `@mutation(multiRow: true)opts out of the PK-coverage check, naming the hazard rather than the mechanism.@valueon input fields marks UPDATE assignment columns; rejected on DELETE / INSERT / UPSERT and mutually exclusive with@conditionon the same field.@lookupKey on INPUT_FIELD_DEFINITIONis retired with a migration diagnostic surfaced at both per-field and per-arg classification sites (theARGUMENT_DEFINITIONuse for Query-sideLookupTableField/SplitLookupTableFieldis untouched). UPSERT is refused upstream atMutationInputResolverwith aRejection.deferredkeyed to R145 (mutation-cardinality-safety-upsert); R141’s compact-constructor UPSERT rejection onMutationBulkDmlRecordFieldbecomes a redundant type-system backstop during the R144-shipped-but-R145-not-yet window. Carrier change:TableInputArg.ofnow takesDmlKind kindand the@value-marked field-name set; partitionslookupKeyFields/setFieldsper verb (UPDATE: complement vs intersection on the@valueset; DELETE / INSERT:setFieldsempty by classifier guarantee).EnumMappingResolver.buildLookupBindingsdrops theDIR_LOOKUP_KEYgate and walks every admissible input field minus a caller-supplied exclude set (the UPDATE@valuenames). Audit producers: two new@LoadBearingClassifierCheckkeys onMutationInputResolver.resolveInput(mutation-input.where-columns-cover-pk,mutation-input.update-set-fields-equal-value-marked) with@DependsOnClassifierCheckconsumers on the eleventia.setFields()walk sites inTypeFetcherGenerator’s UPDATE arms and on the `MutationBulkDmlRecordFieldconstruction site (so any future refactor that branches the bulk path aroundresolveInputsurfaces as an orphaned consumer inLoadBearingGuaranteeAuditTest). Migration: sakila example schema migrated (upsertFilm/upsertFilms/upsertFilmPayloadretired;FilmUpdateInput.title/.descriptioncarry@value; newFilmReleaseYearDeleteInput+deleteFilmsByReleaseYearmultiRow fixture); classifier truth-table fixtures retyped to the new diagnostics; UPSERT execution tests inDmlBulkMutationsExecutionTest,GraphQLQueryTest, andSingleRecordCarrierDmlTest@Disabledwith R145 reference. Tests: pipeline-tierR144_*rows onGraphitronSchemaBuilderTest.MutationDmlCase(PK-coverage admission/rejection,multiRowadmission,@value-on-DELETE rejection,multiRow-on-INSERT rejection,@value+@conditionmutual-exclusion) plusUPDATE_NO_VALUE_FIELDS_REJECTED,UPDATE_EVERY_FIELD_VALUE_MARKED_REJECTED,UPDATE_PARTIAL_COMPOSITE_PK_REJECTED,DELETE_PARTIAL_COMPOSITE_PK_REJECTED,UPDATE_TIA_PARTITIONS_FIELDS_INTO_LOOKUP_AND_SET; execution-tier proofDmlBulkMutationsExecutionTest.deleteFilmsByReleaseYear_multiRowBroadcastsAcrossInputCardinalityasserts|affected rows| == 3while|input rows| == 1against a release-year-keyed broadcast. The Spec’s "empty input +multiRow`" and "DELETE with zero admissible carriers" rejection bullets shipped as unreachable defensive checks (graphql-java rejects empty input types at parse with `"InputObjectType … must define one or more fields", and the per-field loop inresolveInputrejects every non-admissible field shape before the admissible-count check); both branches were removed per the project’s "no error handling for scenarios that can’t happen" rule with a Javadoc note onresolveInputrecording the parser-level guarantee. Docs: newdocs/manual/reference/directives/value.adoccovers the@valuesurface, per-verb validity rules, and the cardinality-safety interaction withmultiRow;DirectiveDocCoverageTestgreen. Out of scope (filed as follow-ups): R145 (mutation-cardinality-safety-upsert) re-admits UPSERT with a designed cardinality story; R146 (mutation-cardinality-safety-unique-index) lifts the PK-only conservative cut to PK-or-unique-index coverage. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R43 commit 5 (
ChildField.RecordTableMethodFieldDTO-parent emit; lift toIMPLEMENTED_LEAVES; R43 In Progress → In Review):SplitRowsMethodEmitter.buildForRecordTableMethodemits the DataLoader rows-method for the new variant: parent VALUES table over the FK source-side columns + the developer’s static@tableMethodcall substituted for the terminalTables.<X>.as("alias")declaration + flatSELECTwithJOIN parentInput ON terminal.<targetSide> = parentInput.<sourceSide>+ scatter viascatterByIdx(list cardinality) orscatterSingleByIdx(single cardinality / LOAD_MANY). The newRowsMethodBody.SqlRecordTableMethodsealed permit gives the body the same framing as the existingSqlRecordTable*siblings (RowsMethodSkeletonemits the empty-input gate + DSL local; the permit’s content references both).TypeFetcherGenerator.buildRecordBasedDataFetcher’s generic constraint loosens from `<T extends TableTargetField & BatchKeyField>to<T extends GraphitronField & BatchKeyField>(takingreturnType: ReturnTypeRef.TableBoundReturnTypeas a parameter) so the existing record-parent DataFetcher emit is shared across the three variants without an interface widening;RecordTableField/RecordLookupTableFieldcall sites thread theirreturnType()explicitly. ThescatterByIdxhelper-emission gate gains aRecordTableMethodField-with-list-cardinality-non-single arm; the existingBatchKeyField.emitsSingleRecordPerKeygate already coversscatterSingleByIdx. Dispatch lift:RecordTableMethodFieldmoves fromSTUBBED_VARIANTStoIMPLEMENTED_LEAVES; the deferred-slug entry retires;generateTypeSpec’s switch arm flips from `builder.addMethod(stub(f))to the pairedbuildRecordBasedDataFetcher+SplitRowsMethodEmitter.buildForRecordTableMethodcalls (mirroringRecordTableField). The variant overridesemitsSingleRecordPerKey()to fold single-cardinality fields onto the single-record-per-key arm, same shape asRecordTableField’s override. Path shape coverage: single-hop `JoinStep.FkJoinis the shipped emit form (the common case, and the only one exercised by the planned pipeline + execution coverage); multi-hop FK paths andJoinStep.ConditionJointerminals surface a runtimeUnsupportedOperationExceptionwith a labelled message, mirroring the table-parentTableMethodFieldcommit-3 emit. Pipeline tests:TableMethodFieldPipelineTest.dtoParentFkAutoDerive_emitsDataLoaderFetcherAndRowsMethodpins the FilmRecord-backed@recordparent + auto-FK-derive + explicit@referencepath shape (the generatedFilmDetailsFetchers.languageDataFetcher signature isCompletableFuture<DataFetcherResult<Record>>wiring aDataLoader/rowsLanguagecall; the rows method body invokesTestTableMethodStub.getLanguage, buildsparentInput, and joins onLANGUAGE_ID).RowsMethodSkeletonTest.rowsMethodBody_sealedSwitchIsExhaustivecount increments to six (the new permit), andUnifiedEmissionPinsTest.rowsMethodEmitter_unifiedSkeletoncount increments to six (the new entry method emits twoRowsMethodSkeleton.buildcalls: one for the emit-able single-hop FK arm, one for the multi-hop / ConditionJoin / empty-path runtime stub). Execution test:GraphQLQueryTest.filmById_detailsForMethod_languageViaTableMethod_routesThroughRecordTableMethodFieldDtoParentEmitexercises the end-to-end DTO-parent path againstrewrite_test. Sakila fixture additions: a new SDL typeFilmDetailsForMethod @record(record: FilmRecord)withfilmId,languageId(declared so the parent SELECT projectsfilm.language_id), andlanguageViaTableMethod: Language @tableMethod(…) @reference(path: [{key: "film_language_id_fkey"}]); a newFilm.detailsForMethod: FilmDetailsForMethodfield that’s aConstructorFieldpassthrough so the parent Film row record flows through to FilmDetailsForMethod’s source-record slot. The query{ filmById(film_id: ["1", "2"]) { filmId detailsForMethod { filmId languageId languageViaTableMethod { languageId name } } } }returns the seededlanguage_id=1/name="English"for both films, confirming the developer’stableMethodLanguage()table is correctly joined against the lifted FK keys. Out of scope (called out, not regressed): multi-hop FK path emit andConditionJointerminal emit forRecordTableMethodField(the runtime stubs are loud, not silent); execution-tier coverage for the@sourceRowarm (a separate fixture with a hand-written lifter is a candidate follow-up; the classifier branch is already covered byRecordTableMethodFieldCase). R43 moves In Progress → In Review with this commit. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 (1683 graphitron tests + sakila-example compile + execute tiers all passing). -
R43 commit 4 (
ChildField.RecordTableMethodFieldvariant added, emit stubbed): New sealed-permit onChildFieldcovers child@tableMethodfields on@record(non-table) parents ; the DTO-parent sibling ofChildField.TableMethodField. The record carriesparentTypeName,name,location,ReturnTypeRef.TableBoundReturnType returnType,List<JoinStep> joinPath,MethodRef method,SourceKey sourceKey,LoaderRegistration loaderRegistration,Optional<ErrorChannel> errorChanneland implementsChildField, MethodBackedField, BatchKeyField, WithErrorChannel. It wears the shared@DependsOnClassifierCheck("tablemethod-resolver-return-is-table-bound")audit annotation (the resolver-side rejection of non-table returns underwrites the narrowed component type the same way it does forTableMethodField/QueryTableMethodTableField). Classifier:FieldBuilder.classifyChildFieldOnResultTypegrew a new@tableMethodbranch placed before the@sourceRowbranch, so both directives can coexist on the same field (their roles are complementary:@sourceRowprovides the batch-key lifter;@tableMethodprovides the developer’s static jOOQ table method). Two admit arms: (a) JooqTableRecordType parent + unique catalog FK between parent’s table and@tableMethodreturn-type table ; auto-derives theSourceKeyvia the existingderiveFkRecordParentSourcehelper, sameWrap.Row+Reader.ColumnReadshapeRecordTableField’s FK arm produces; (b) free-form DTO parent (`PojoResultType/JavaRecordType) +@sourceRow(className:, method:); delegates toSourceRowDirectiveResolverfor the lifter-derivedSourceKey(Wrap.Record+Reader.SourceRowsCall). Both arms compose with@reference(path:)for explicit FK chains. The same last-hop-target check from the table-parent branch applies. A free-form DTO without@sourceRowand without FK metadata produces a structuredUnclassifiedFieldAUTHOR_ERROR enumerating the three lift options (typed jOOQ TableRecord backing,@sourceRow, or a typed accessor). Dispatch / emit:STUBBED_VARIANTSgains an entry keyed onRecordTableMethodField.classwith plan slugtablemethod-child-table-bound(commit 5 will lift it toIMPLEMENTED_LEAVES);generateTypeSpec’s child switch arm routes `RecordTableMethodFieldthroughstub(f)so schemas exercising the variant fail at validate-time with the standard deferred message rather than crashing at request time. Validator:GraphitronSchemaValidatorgained a new switch arm +validateRecordTableMethodFieldhelper applying the existingvalidateReferencePath+validateCardinalitychecks (mirror ofvalidateTableMethodFieldfor the table-parent sibling). Carrier-plumbing:MappingsConstantNameDedup.withResolvedChannelrebuilds the new variant via its existingWithErrorChannelsealed switch. Tests: newRecordTableMethodFieldCaseenum inGraphitronSchemaBuilderTestpins three classifier shapes ;JOOQ_TABLE_RECORD_PARENT_AUTO_FK(FilmRecord parent +getInventoryauto-FK to Inventory),JOOQ_TABLE_RECORD_PARENT_EXPLICIT_REFERENCE(FilmRecord parent +getLanguagewith explicit@reference(path: [{key: "film_language_id_fkey"}])), andFREE_FORM_PARENT_NO_SOURCEROW_REJECTED(DummyRecord parent +getInventorywithout@sourceRowor FK metadata → rejection naming the three lift options).TestTableMethodStubgainsgetInventory()returningInventory.classfor the new tests.VariantCoverageTestconfirms the new sealed leaf has classification coverage; the existing partition tests (GeneratorCoverageTest.everyGraphitronFieldLeafHasAKnownDispatchStatusandnotImplementedReasonsContainsOnlyConcreteSealedLeaves) stay green because the new class is keyed inSTUBBED_VARIANTSrather thanIMPLEMENTED_LEAVES. Out of scope (kept under R43 commit 5): DTO-parent emit reusingRecordTableField’s DataLoader-keyed batch pattern with the developer’s static method substituted; pipeline-tier fetcher emission tests; execution-tier coverage against `rewrite_test; moveRecordTableMethodFieldfromSTUBBED_VARIANTStoIMPLEMENTED_LEAVES. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 (1673 graphitron tests passing). -
R43 sub-commit (FK-projection injection for child
@tableMethodon table-bound parents): Closes the execution-tier gap commit 3 left open. The child@tableMethodfetcher readsparentRecord.get(DSL.name("<sourceSqlName>"), …)for parent-row correlation; without injecting the FK source-side column into the parent SELECT, the read throwsIllegalArgumentException: Field "<col>" is not contained in row type ("<schema>"."<parentTable>"."<pk>")whenever the user’s SDL selection omits the FK column. Fix:TypeClassGenerator.collectSourceKeyColumnsgeneralises tocollectRequiredProjectionColumnsand gains aChildField.TableMethodFieldarm that extracts the single-hopJoinStep.FkJoin’s `sourceSideColumns()and threads them through the existingrequiredProjectionColumnspipeline ; sameif (!fields.contains(table.$L)) fields.add(table.$L)idempotent-append idiom Split* fields already use for theirSourceKeycolumns. Only single-hopFkJoinshapes contribute: multi-hop andConditionJoinpaths surface a runtimeUnsupportedOperationExceptioninbuildChildTableMethodFetcheranyway, so projecting their first hop would synthesise dead columns. NestingField recursion preserved so nested@tableMethodfields under a non-table-bound nested type get their FK columns into the outer table-class’s$fields. Pipeline tests:TableMethodFieldPipelineTestgainssingleFkAutoInferred_parentDollarFieldsProjectsFkSourceColumn(auto-FKInventory→FilmpinsFILM_IDonInventory.$fields) andexplicitReferencePathSingleHopFk_parentDollarFieldsProjectsFkSourceColumn(explicit@reference(path: [{key: "film_language_id_fkey"}])pinsLANGUAGE_IDonFilm.$fields); both use the sharedTypeSpecAssertions.appendsRequiredColumnhelper that already pins the same idiom for Split* fields. Execution tests:GraphQLQueryTestgainsinventoryById_filmViaTableMethod_correlatesParentRowViaInjectedFkProjection(threeInventoryrows each correlating to their matchingFilmbyinventory.film_id; assertsfilm.filmIdandfilm.titleper row) andfilmById_languageViaTableMethod_correlatesParentRowViaExplicitReferencePathFk(twoFilmrows each correlating toLanguageviafilm.language_id; assertslanguage.languageId == 1and strippedlanguage.name == "English"). Both queries deliberately omit the FK column from their SDL selection so the projection-injection path is exercised end-to-end. Out of scope (kept under remaining R43 commits): the newChildField.RecordTableMethodFieldvariant for DTO-parent batching (commit 4); DTO-parent emit + execution coverage (commit 5). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R43 commit 3 (child table-bound-parent lift:
ChildField.TableMethodFieldmoves toIMPLEMENTED_LEAVES): NewTypeFetcherGenerator.buildChildTableMethodFetcheremits a per-row fetcher modelled on the root-sitebuildQueryTableMethodFetcher: declareparentRecord = (Record) env.getSource(), call the developer’s static@tableMethodto derive the target table local, declaredsl, build a parent-correlationConditionfrom the resolvedJoinStepchain, then SELECT the typed$fieldsprojection from the developer-returned table with the correlation as WHERE. The new helperbuildTableMethodParentCorrelationwalks eachJoinSlotof the (single)FkJoinhop and emitstable.<targetSide>.eq(parentRecord.get(DSL.name("<sourceSqlName>"), <columnClass>.class)), threading the typedparentRecord.get(name, Class)overload so the resultingConditiontype-checks againstField<T>.eq(T). Composite FKs AND across slots; an empty-slots fallback emitsDSL.noCondition()so the catalog-unavailable case fails loudly at runtime. Path shape coverage: single-hopJoinStep.FkJoinis the shipped emit form (the common case, and the one exercised by R43’s planned pipeline + execution coverage). Multi-hop FK paths andJoinStep.ConditionJointerminals are still accepted by the classifier (GraphitronSchemaBuilderTest.TableMethodFieldCase.LIST_RETURN/CONNECTION_RETURN/WITH_CONDITION_PATH) but the emitter surfaces a runtimeUnsupportedOperationExceptionwith the shape label ("empty joinPath"/"multi-hop join path"/"ConditionJoin path") so the gap is loud rather than silent. Three@DependsOnClassifierCheckannotations pin the producer/consumer contracts (tablemethod-resolver-return-is-table-boundfor the narrowed return type,service-catalog-strict-tablemethod-returnfor the no-downcast emit,service-catalog-tablemethod-must-be-staticfor the static-call shape) plusfk-join.slots-oriented-source-and-targetonbuildTableMethodParentCorrelation. Dispatch:IMPLEMENTED_LEAVESgainsChildField.TableMethodField.class;STUBBED_VARIANTSloses its entry (thetablemethod-child-table-boundplan-slug binding retires);generateTypeSpec’s child switch arm flips from `builder.addMethod(stub(f))tobuilder.addMethod(buildChildTableMethodFetcher(ctx, f, outputPackage)). Pipeline tests: newTableMethodFieldPipelineTesttwo cases:singleFkAutoInferred_emitsFetcherMethod(Inventory.film with single-FK auto-inference) andexplicitReferencePathSingleHopFk_emitsFetcherMethod(Film.language with@reference(path: [{key: "film_language_id_fkey"}])); both assert the generatedFilmFetchers/InventoryFetcherscontain a fetcher method under the field name with the standard(DataFetchingEnvironment)signature, the correctDataFetcherResult<Record>return type, and a body that invokes the developer-authored static method.TableMethodFieldValidationTest’s three `stubbedErrorcases (NO_PATH,WITH_FK_PATH,WITH_CONDITION_ONLY) flip to assert empty errors ;TableMethodFieldis no longer inSTUBBED_VARIANTS, soGraphitronSchemaValidator.validateVariantIsImplementedis silent for these fixtures. Compile-tier coverage: sakila-example schema gainsInventory.filmViaTableMethod: Film @tableMethod(…)(auto-FK single-hop) andFilm.languageViaTableMethod: Language @tableMethod(…) @reference(path: [{key: "film_language_id_fkey"}])(explicit single-hop path);SampleQueryServiceaddstableMethodFilm()/tableMethodLanguage()returningTables.FILM/Tables.LANGUAGEdirectly; thegraphitron-sakila-examplecompile step type-checks the generated fetcher bodies against the real jOOQ classes (FilmFetchers.languageViaTableMethod and InventoryFetchers.filmViaTableMethod). Out of scope (deferred to a follow-up): runtime execution-tier coverage requires the parent fetcher to project the FK source column (e.g.inventory.film_id) when the child@tableMethodfield is in the selection set; today the parent’s$fieldswalks only user-requested SDL fields andparentRecord.get(DSL.name("film_id"), …)fails withIllegalArgumentException: Field "film_id" is not contained in row type ("public"."inventory"."inventory_id"). Mechanism for FK-column injection (analogous to the projection synthesis thatNodeIdReference/CompositeColumnReferencealready do at classify time) is a separable concern from the lift itself; commit 3 ships the emit + dispatch lift, and a follow-up R43 sub-commit will land FK-projection injection so the sakila fixture’s runtime path comes online. Out of scope (kept under remaining R43 commits): multi-hop FK path emit; ConditionJoin emit; the newChildField.RecordTableMethodFieldvariant for DTO-parent batching (commit 4); DTO-parent emit (commit 5). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 (1640+ graphitron tests + sakila-example compile + execute tiers all passing). -
R43 commit 2 (path resolution + last-hop-target validation for
@tableMethodat child sites):FieldBuilder.classifyChildFieldOnTableType’s `@tableMethodarm reorders the resolver andparsePathcalls so the return-type table is known before path resolution:tableMethodResolver.resolveruns first, thenctx.parsePath(fieldDef, name, tableType.table().tableName(), tb.returnType().table().tableName(), buildWrapper(fieldDef).isList())runs with the target table populated, exercising the existing auto-FK inference branch inparsePath(findForeignKeysBetweenTableswithdirectiveAbsent=true). The classifier adds a last-hop-target check: if the resolved path is non-empty and its last hop is aJoinStep.FkJoin, the hop’stargetTable().tableName()must equal the return-type’s table name (case-insensitive), else surface"@tableMethod @reference path: last hop lands on '<X>' but @tableMethod’s return type is bound to table '<Y>'".JoinStep.ConditionJoinlast hops are exempted from the structural check by design ; the condition method’s signature is the implicit contract there. Three accepted shapes (matching@referencesemantics): (a) no@reference+ exactly one FK between parent and return-type tables → single-hop FkJoin auto-inferred; (b) explicit@reference(path: [{key: "…"}, …])→ walks each hop, last hop must land on return-type table; (c)@reference(path: [{condition: {className, method}}])→ ConditionJoin terminal. Three rejection shapes: ambiguous FK (multiple FKs between parent and target, no@reference) surfaces the existingfkCountMessage"multiple foreign keys found between tables …" with directive-absent guidance; missing FK + no@referencesurfacesfkCountMessage’s zero-FK arm; last-hop-target mismatch surfaces the new structural rejection. Test fixtures updated for the new behaviour: `GraphitronSchemaBuilderTest.TableMethodFieldCase.SINGLE_RETURN/LIST_RETURN/CONNECTION_RETURN/TABLE_METHOD_FIELD_CONTEXT_ARGSadd explicit@reference(path: …)(Film→Languagehad two FKs and would now reject as ambiguous;Film→Actorhas no direct FK and would now reject as missing). New pipeline tests:TableMethodFieldCase.WITH_AUTO_FK_INFERENCE(Inventory→Film single-FK auto-infers a single-hop FkJoin landing onfilm);TableMethodFieldCase.WITH_CONDITION_PATH(@reference(path:[{condition:…}])resolves to ConditionJoin). New rejection tests inUnclassifiedFieldCase:TABLEMETHOD_CHILD_AMBIGUOUS_FK_REJECTED(Film→Language with no@reference);TABLEMETHOD_CHILD_MISSING_FK_REJECTED(Film→Actor with no@reference);TABLEMETHOD_CHILD_LAST_HOP_MISMATCH_REJECTED(@reference(path:[{key:"film_language_id_fkey"}])on a field declaredActor→ last-hop-lands-on-language rejection). No emit change:TypeFetcherGenerator.STUBBED_VARIANTSstill mapsChildField.TableMethodField.classto the deferred slug; the dispatch ingenerateChildFetcherstill routes tostub(f). The lift toIMPLEMENTED_LEAVESis R43 commit 3. Out of scope: emit (commit 3); the newChildField.RecordTableMethodFieldvariant for DTO-parent batching (commit 4); DTO-parent emit (commit 5). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R43 commit 1 (directive flattening + method-signature rewrite at the root site): Retired
BuildContext.ARG_TABLE_METHOD_REF; flattened the@tableMethoddirective indirectives.graphqlsfrom(tableMethodReference: ExternalCodeReference!, contextArguments:)to(className: String!, method: String!, argMapping: String, contextArguments: [String!]), mirroring@sourceRow.TableMethodDirectiveResolver.resolveparses the flat args inline (sibling toSourceRowDirectiveResolver);parseExternalRefstays as-is for@service/@externalField. Method-signature contract: developer’s@tableMethodstatic method now receives only GraphQL field arguments andcontextArguments:values ; no Table parameter.ServiceCatalog.reflectTableMethodgains a newTableSlotPolicy { REQUIRED, FORBIDDEN }parameter so the three call sites (TableMethodDirectiveResolver, two@conditionpaths inConditionResolver, two@conditionpaths inBuildContext.parseCondition*) pick their semantics:@tableMethodpasses FORBIDDEN (reject anyTable<?>parameter; nofoundTablerequirement),@conditioncallers pass REQUIRED (keep the originalParamSource.Tableslot +foundTableinvariant + the reserved-Table-slot argMapping typo guard, factored as the newcheckConditionOverrideTargetshelper).TypeFetcherGenerator.buildQueryTableMethodFetchernow passesnullfortableExpressiontoArgCallEmitter.buildMethodBackedCallArgsand the emitted call drops the leadingTables.<NAME>argument; the body shape staysvar table = ClassName.method(<args>)with<args>now sourced exclusively fromParamSource.Arg/ParamSource.Contextslots. The flat form drops the deprecatedname:alias on@tableMethod(the existing@sourceRowprecedent already shipped without it). LSP canonical overlay (LspVocabulary.CanonicalOverlay) gains three bindings paralleling@sourceRow:tableMethod.className → ClassNameBinding,tableMethod.method → MethodNameBinding(tableMethod.className),tableMethod.argMapping → ArgMappingBinding. Test-fixture migration: every@tableMethod(tableMethodReference: {className: "X", method: "Y"})occurrence inGraphitronSchemaBuilderTest,ServiceRootFetcherPipelineTest,TableMethodFieldValidationTest,DiagnosticsTest,ClassNameCompletionsTest, and the sakila-example schema flattens to@tableMethod(className: "X", method: "Y").TestTableMethodStubstatic methods drop their leadingTable<?>parameter.SampleQueryService.popularFilmsrewrites from(Film filmTable, Double minRentalRate)to(Double minRentalRate), derivingTables.FILMinternally.ServiceCatalogTest’s `reflectTableMethod_*cases thread the new policy parameter;reflectTableMethod_overrideTargetingTableSlot_rejectednow exercises the REQUIRED policy viaTestConditionStub.argCondition.TypeFetcherGeneratorTest.queryTableMethodTableField_emittedFetcher_*drops theParamSource.Tableslot from its handcraftedMethodRef. The LSPlegacyName_unresolved_tableMethodtest retires (name:alias is gone). User-facing docdocs/manual/reference/directives/tableMethod.adocrewrites the SDL signature and examples to the flat form and adds a "Method-signature contract" section pinning the no-Table-parameter rule.docs/manual/how-to/external-code.adocupdates the per-directive slot-name table to distinguish flat-form (@tableMethod,@sourceRow) fromExternalCodeReference-shaped directives. R43 status was moved Ready → In Progress in55b5d5fahead of this commit. Out of scope (kept under remaining R43 commits): path resolution + last-hop-target validation; child table-bound-parent lift (TypeFetcherGenerator.STUBBED_VARIANTSmembership forChildField.TableMethodFieldremains); newChildField.RecordTableMethodFieldvariant. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R100 (
362719eimplementation;8498b89In Progress → In Review): LSP coverage for@node(keyColumns:)and@nodeId(typeName:). Per-keystroke responsiveness layer on top of the existing classifier-rejection paint (validatorDiagnosticskeeps full coverage at the rebuild tier). Two canonical-overlay deltas plus one newBehaviorarm:@node(keyColumns:) → CatalogColumnBindingsoFieldCompletionsandDiagnostics.validateCatalogColumnauto-fire over the type’s@table-backed jOOQ column list;@nodeId(typeName:) → new Behavior.NodeTypeBinding()siblings-by-keyset toCatalogColumnBinding/CatalogTableBinding, with one new completion provider (NodeTypeCompletions) and one new arm inDiagnostics.dispatchthat reads a newCompletionData.nodeMetadata()map.CatalogBuilderwalks the assembled schema’sGraphQLObjectType`s and records pre-deduction `(typeId, keyColumns)per@node-bearing type; classifier-deduced values (containing-type / unique-table / PK inference) stay invisible to in-editor feedback by design.LspVocabulary.leafCoordinates/descendLeavesfan out rawlist_valueAST nodes into oneLeafper scalar element soCatalogColumnBindingdispatches per-element onkeyColumns: […]; the contract pin “Leaf.valueNode` is the scalar value node, never an enclosinglist_value” lifts to a universal property of the leaf walk. `Hovers.valueNodeFormirrors the descent so cursor inside a list element highlights the element, not the whole list; newnodeTypeHoverarm renders the target type’stypeId+ key-column list with each column’sgraphqlTypepulled fromCompletionData.Column. Two backwards-compatCompletionDataconstructors retained (existing 3-arg pattern extended with a 4-arg shim for tests not carrying the new map). Tests five wire-shape integration cases (FieldCompletionsTest.nodeKeyColumnsCompletionInsideListLiteralReturnsTableColumns;DiagnosticsTest.nodeKeyColumns_unknownElement_producesError/_allValid_producesNoError,.nodeIdTypeName_unknownType_producesError/_knownNodeType_producesNoError/_emptyNodeMetadata_suppressesUnknownTypeDiagnostic;HoversTest.nodeKeyColumnsHover_insideListElement_showsColumnMetadata/.nodeIdTypeNameHover_resolvesTypeIdAndKeyColumns); per-provider unit cases inNodeTypeCompletionsTest(3); leaf-walk fan-out pin inLspVocabularyTest.leafCoordinates_listValueFansOutOneLeafPerElement; catalog-sideNodeMetadatacoverage inCatalogBuilderTest(3 cases ; author-supplied capture, omitted-axes-stay-null, non-@nodetypes omitted). Reference docs gain "Editor support" subsections onnode.adocandnodeId.adoc. Out of scope (called out, not regressed):@node(typeId:)cross-schema duplicate validation stays on rebuild path;@nodeId(typeName:)deduction rules (containing-type / unique-table inference) stay invisible to LSP;@nodeplacement and PK-defaulting structural validation stays on the rebuild path;@nodeId(typeName:)diagnostic conflates "type doesn’t exist" and "type exists without@node`" into one message and tests only the unknown-type case (the spec listed both scenarios; the type-exists-without-@node` case is incremental coverage of the same code path and a candidate follow-up);nodeTypeHover.columnGraphqlTypedoes case-insensitive across all tables rather than scoping to the@nodetype’s@table(latent, not triggered by Sakila; candidate follow-up). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R141 (
d58d46cIn Progress → In Review carrier-shape lift + new sealed leaf + tests;83cd67arework: order-preservation PK-keyed-map indirection inFetcherEmitter.buildSingleRecordTableFetcherValue’s `Cardinality.MANYarm): Admit bulk-input mutations with a single payload carrier wrapping a list-shaped data field. NewMutationField.MutationBulkDmlRecordFieldsealed leaf (sibling ofMutationDmlRecordField) classifies(tia.list() == true, dataField.wrapper().isList() == true, kind ∈ {INSERT, UPDATE}); carrier-shape lift introduces sealedCarrierFieldRole(permitsDataChannel,ErrorChannelRole) withSingleRecordCarrierShapecarryingList<CarrierFieldRole>under compact-ctor invariants (exactly-one DataChannel, at-most-one ErrorChannelRole, distinct field names), consolidating the previously parallel data + error walks into a single unified walk inBuildContext.tryResolveSingleRecordCarrier(the carrier-sideresolveErrorChannelcall site retires; the standalone method stays for the four non-carrier callers). Compact-ctor on the new leaf rejects DELETE (incorrect-by-construction) and UPSERT (deferred to R145 under R144’s cardinality-safety regime); UPSERT bulk-carrier case surfaces as a classify-time author-facing rejection rather than letting the compact-ctor throw. Single-input + list-data-field rejects as new Invariant #16 insideMutationInputResolver.validateReturnType. Emit strategy: per-row DML insidedsl.transactionResult(…)accumulating PKs intoResult<RecordN<PK>>in input order (N+1 statements: N per-row DML + 1 response SELECT); order preservation lifted from a Postgres-scan-order coincidence to a property of the emitted Java via PK-keyed-map indirection inFetcherEmitter.buildSingleRecordTableFetcherValue’s `Cardinality.MANYarm (re-key SELECT result intoMap<PK, Record>, iterate the upstream input-orderedResult<RecordN<PK>>to project intoList<Record>in input order). UPDATE no-match throwsIllegalStateExceptionto keepacc.size() == in.size()invariant. Audit. New load-bearing classifier-check keysingle-record-carrier-shape.roles-exhaustively-classified(producer ontryResolveSingleRecordCarrier, consumers onGraphitronSchemaBuilder.registerCarrierDataFieldandTypeFetcherGenerator.buildMutationBulkDmlRecordFetcher);mutation-dml-record-field.data-table-equals-input-tableextends across both record-carrier leaves. NewCarrierFieldRoleCoverageTestaudits permit dispatch across consumers via grep-on-source-name; reflection-based hardening tracked at R151. Tests. Three classifier truth-table rows:MUTATION_BULK_DML_RECORD_FIELD(admit),DML_INSERT_SINGLE_LIST_DATA_REJECTED(Invariant #16),DML_INSERT_LIST_PAYLOAD_NO_CARRIER_FIELD_ROLE_REJECTED. Three execution tests inDmlBulkMutationsExecutionTest:bulkInsertWithThreeRowsInNonPkOrderPreservesInputOrderInResponse(N=3 load-bearing order assertion),bulkInsertWithSingleRowExercisesBulkLeafPath(N=1 sanity),bulkUpdateWithThreeRowsInNonPkOrderPreservesInputOrderInResponse(UPDATE order assertion). Sakila fixture gainsFilmsPayload { films: [Film!] }+createFilmsPayload/updateFilmsPayloadmutations. Out of scope (deferred): per-row error correlation (R12 flat-error contract preserved); affected-row-count / clientMutationId sibling permits (each is a newCarrierFieldRolepermit + classifier rule); UPSERT bulk-carrier admission (R145);@servicebulk-carrier symmetric path; sealed-on-kind / sub-taxonomy refactors of both record-carrier leaves. Defers consumer-side@DependsOnClassifierCheckannotations against R12’s plannederror-channel.*keys; R12 lands both halves in one commit when it ships, referencing R141’sErrorChannelRolepermit Javadoc as the trust-statement anchor. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R150 (
aa7e1b0implementation;c8bdbe5SDL-driven invariant tightening;e96a05cSDL Scalar vs Input Object only;bfdde5epermanent Map rejection;2c2b37acycle detection + public-class check;bb79b63polish ; typedLinkageError/ClassNotFoundException, nested-class.→$retry,createBean/createBeanListplural naming, FieldKey inlined): Instantiate service-layer input beans at the@servicefetcher boundary. Closes the silentClassCastException: LinkedHashMap cannot be cast to <ConsumerBean>gap when a@servicemethod’s Java parameter (single orList<Bean>) is a consumer-authored class mirroring an SDLinputtype. NewCallSiteExtraction.InputBeansealed-variant arm carries the beanClassName, theTarget(RECORD/JAVA_BEAN constructor shape), and per-SDL-fieldFieldBinding`s. `InputBeanResolverpost-processes a resolvedMethodRef.Service(sibling toEnumMappingResolver.enrichArgExtractions), driving classification off the SDL side: GraphQL scalar args (including custom scalars wired via@scalarType) stay onDirectso graphql-java’s coercion delivers the consumer’s declared Java type, and GraphQL input-object args classify asInputBeanor reject loudly at generation time. Rejections are exhaustive and structural ; non-public bean classes, missing record components, classes without a public no-arg constructor, recursive shapes (guarded by a path-scopedvisitedset, preventsStackOverflowErrorat gen time), Map/JDK/org.jooq./enum/array element types paired with input-object SDL slots, and list-cardinality mismatches.Map<K, V>is permanently rejected as a service-boundary anti-pattern; consumers wanting open-ended-JSON semantics declare a custom scalar via@scalarType.InputBeanInstantiationEmitteremits onecreateBean(Map<String, Object>)+ onecreateBeanList(Object)helper per unique bean class on the enclosing*Fetchersclass ; dedup-by-class viacollectTransitivelywalks nested input-object leaves. Records use positional canonical-ctor; JavaBeans use no-arg +set<X>setters.ArgCallEmitter.buildArgExtractionroutes theInputBeanarm to the helper call. *Cycle-prevention invariant preserved: helpers reference only JDK types and the consumer’s service-package class; no helper imports a graphitron-emitted record (R94 compatibility). Tests four-tier: L1TypeFetcherGeneratorTestpins helper signature + record/JavaBean target +createFooListplural naming + transitive dedup; L2GraphitronSchemaBuilderTestcovers singular/list InputBean classification plusSERVICE_MAP_PARAM_FOR_INPUT_OBJECT_REJECTED,SERVICE_RECURSIVE_BEAN_REJECTED,SERVICE_NON_PUBLIC_BEAN_REJECTEDarms; L3graphitron-sakila-exampleaddsFilmReviewDetailsInput+FilmReviewTagInputSDL types andsubmitFilmReviewWithDetails(details: FilmReviewDetailsInput!)mutation, with consumer-authoredFilmReviewDetailsrecord +FilmReviewTagrecord ingraphitron-sakila-servicecompiling against the generated helper; L4GraphQLQueryTest.submitFilmReviewWithDetails_routesThroughInstantiatedInputBeanround-trips a nested-list-bearing bean through a real GraphQL mutation, asserting service body sees typed scalar values. Out of scope (called out, not regressed):@serviceparameter as a jOOQTableRecordsubclass currently routes through the JavaBean setter path (not idiomaticrecord.from(map); tracked as a follow-up); recursion is head-only ; a@serviceparameter whoseargMappingis a multi-segment dot-path stays on the legacyDirectarm even when the leaf SDL type is an input object; SDLoneOfpolymorphic inputs and builder-pattern target classes deliberately deferred. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R108 (
a60c58aimplementation): Per-variant projection on polymorphic fields. The multi-table polymorphic dispatcher’s Stage-2 per-typename SELECT now threads the parentDataFetchingFieldSelectionSetthroughPolymorphicSelectionSet.restrictTo(source, concreteTypeName)(new emitted helper at<outputPackage>.util.PolymorphicSelectionSet, generated byPolymorphicSelectionSetClassGeneratorundergenerators/util/, registered inGraphQLRewriteGeneratornext toConnectionHelper), so each per-typename SELECT projects only columns whoseSelectedField.getObjectTypeNames()contains that participant. The helper is a delegating wrapper that materially overrides onlygetFieldsGroupedByResultKey()and delegates every otherDataFetchingFieldSelectionSetmethod to the source, keeping the nested-projection recursion in$fields(which walkssf.getSelectionSet()) intact and avoiding a widened$fieldssignature. Same-table interface emit site atTypeFetcherGenerator.buildInterfaceFieldsListintentionally untouched (per-spec carve-out: theLinkedHashSetdedup masks over-selection in every currently-exercised fixture); javadoc cross-reference notesrestrictTois reusable as-is when a fixture exercises the break-the-dedup shape. Tests four-tier:PolymorphicProjectionFilterPinTest(unit,UnifiedEmissionPinsTestprecedent ; folder-wide$T.restrictTo(env.getSelectionSet()count == 1, single-file$$fields(env.getSelectionSet()inMultiTablePolymorphicEmitter.javacount == 0);PolymorphicSelectionSetClassEmitTest(pipeline, structural pin of the emitted class ; name, modifiers,restrictTosignature, private no-arg constructor, private static finalFilterednested type implementingDataFetchingFieldSelectionSet);PolymorphicNestingFilterTest(pipeline, asserts exactly onePolymorphicSelectionSetreference per Stage-2 helper body, encoding "no further filter needed at depth");RecordParentMultiTablePolymorphicPipelineTestextended with an asymmetric-fragment fixture (Inventory + Content sharingfilmIdbacked by different columns on different tables) driving full SDL → classify → emit;PolymorphicProjectionQueryTest(execution, SQL-capture via jOOQExecuteListener; asymmetric-Customer asserts Staff Stage-2 SELECT does not contain"staff"."first_name", asymmetric-Staff pins the inverse, symmetric keeps both; SELECTs picked by the per-typename"customerinput"/"staffinput"VALUES alias to ignore Stage-1’s narrow UNION ALL);GraphQLQueryTest.addressOccupants_asymmetricFragment_responsePayloadDropsInactiveBranch(behavioural pin on the response map). Stage-1 narrow SELECT, DataLoader-batched vs inline arms,requiredProjectionColumns, and synthetictypename/sort__/idxprojections all untouched (added outside the$fieldscall or inside$fieldsoutside the selection switch). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R147 (
4fadc3dimplementation;2450971simplify: drop the backward-compatDiagnostics.computeoverload): SurfaceGraphitronSchemaValidatorerrors and warnings as LSP diagnostics.GraphQLRewriteGenerator.buildOutput()now runs the validator over the same classified bundle that yields the catalog and snapshot, packaging the result as a newValidationReport(errors, warnings, sourceUris)record alongside aBuildArtifacts(catalog, snapshot)split ofBuildOutput; the precomputedsourceUriscanonical-URI set letsDiagnostics.computeshort-circuit per file with oneSet.contains.Workspace.setBuildOutput(BuildArtifacts, ValidationReport)replaces the oldsetCatalog/setCatalogAndSnapshotoverloads, atomically swapping all three volatile refs;DevMojoroutes both the schema-save and classpath triggers through the unified setter, so unresolved-@service-class errors surface in the editor on the nextmvn compilewithout waiting for a schema save. Severity mapping is an exhaustiveswitchover theRejectionsealed hierarchy (AuthorError/InvalidSchema→Error,Deferred→Warning);BuildWarningmaps toWarning. Freshness-aware silence policy mirrors R139: validator diagnostics fire underBuilt.Currentonly, silent underUnavailableandBuilt.Previous. Source attribution is"graphitron-validator", distinct from"graphitron-lsp". Two new paired classifier-check keys (source-location.absolute-path-source-nameonRewriteSchemaLoader,validation-report.canonical-urionValidationReport.canonicalUri) pin the cross-module invariants the LSP filter relies on. Tests:ValidatorDiagnosticsTestcovers severity perRejectionpermit, per-file filtering, freshness gating, no-usable-location drop, and the empty-report-clears-previous-diagnostics contract at compute-call level;RejectionSeverityCoverageTestpins exhaustiveness reflectively;WorkspaceTestgetssetBuildOutputswap tests;ValidationReportTestcovers thefromfactory and the canonical-URI helper;CatalogRefreshTestmigrates to the new setter. Self-review surfaced two follow-up items filed mid-implementation: R148 (source-location-skips-description.md, Backlog/bug) ;FieldDefinition.getSourceLocation()returns the start of the description block when one is present, so diagnostics on documented fields highlight the doc block rather than the field; R149 (r147-followup-end-to-end-publish-diagnostics-tests.md, Backlog/test) ; end-to-end LSPpublishDiagnosticswire-test andGraphQLRewriteGeneratorTestforbuildOutput()report population were deferred. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R143 (
e670fb9): Surface a date column on the rolled-up roadmap.Itemgains nullablecreated: LocalDate/lastUpdated: LocalDateparsed viaItem.from.parseDate(accepts both SnakeYAML’s auto-parsedjava.util.Dateshape and bare-string YAML; absent passes, malformed throws naming slug+key+value).runCreatestamps both dates today; newstatussubcommand (runStatus+ the pure, package-visibleapplyStatusTransition) resolves slug orR<n>viaresolveItemFile, validates target+transition againstTARGET_STATES/ALLOWED_TRANSITIONS, writes newstatus:+ freshlast-updated:, leavescreated:strictly untouched (never invented for pre-R143 items), and regenerates README;DoneandDiscardedare rejected as targets perworkflow.adoc. MarkdownrenderActivegains anUpdatedcolumn;appendBacklogLineemits<sub>updated Y-M-D[, created Y-M-D]</sub>between description andblocked by:. AsciiDocrenderAdocStatusBoardbecomes[cols="1,4,1,1,1"]with the new column; backlog adoc emits italic(updated …); the plan-page attribute box gainsCreated/Updatedrows (suppressed when absent)..claude/skills/roadmap/SKILL.mdrewritten to invoke thestatussubcommand instead of hand-editing front-matter;workflow.adocgains a bullet on the auto-stamp under "Item file conventions". Tests:RoadmapDateColumnTest(21 cases) covers create stamping, status preserving created in both present/absent shapes, rejection of invalid transitions and ofDone/Discarded, slug +R<n>resolution, all four renderer cells in both markdown and AsciiDoc, the plan-page attribute box, and the parser’s "absent passes, malformed fails" semantics. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R140 (
036772f): Publish leaf-coverage report from CI.rewrite-build.ymlbuildjob grows two trunk-gated steps aftermvn verify -Plocal-db;Regenerate leaf-coverage reportruns the roadmap-tool exec against the in-workspacetarget/leaf-coverage.jsonltraces, thenUpload leaf-coverage artifactuploads the regeneratedgraphitron-rewrite/roadmap/inference-axis-coverage.adocas theinference-axis-coverageartifact withif-no-files-found: error. Two new jobs in the same workflow:docs-build(needsbuild, trunk-gated, downloads the artifact over the committed placeholder, builds the docs module with-DskipTests, uploads the Pages artifact) anddocs-deploy(needsdocs-build, trunk-gated,pages: write+id-token: write,pagesconcurrency,github-pagesenvironment,actions/deploy-pages@v4)..github/workflows/deploy-docs.ymldeleted; the consolidation sidesteps theworkflow_run-on-default-branch constraint that blocked the original R132 sketch (rewrite workflow files do not live onmain).workflow_dispatchdoes not survive; manual re-deploy is via the Actions UI re-run on the most recent successful trunk run.inference-axis-coverage.adocprose updated to describe the live publish chain and explain that the in-git file stays as a non-data placeholder so local doc builds and PR-preview renders find a file at the expected path. Cross-spec: when R133 flips the leaf-coverage profile to opt-in, theRegenerate leaf-coverage reportstep here will need-Pleaf-coverageadded; R133’s own spec already owns that coordination. Pre-merge verification limited to YAML parse + additive/trunk-gated reasoning + PR-run isolation; full deploy-path verification is post-merge againsthttps://sikt-no.github.io/graphitron/roadmap/inference-axis-coverage.html. -
R142 (
04a649buser-directive arms wired throughDirectiveResolution;4ac157eself-review fixes pinning bundled-shadows-snapshot precedence on the hover surface;a39ce93In Progress → In Review): Phase 2 of the LSP schema-snapshot side-channel (R139 was phase 1). Three more LSP consumers now read the snapshot through the sealedDirectiveResolution.{Bundled | User | Unknown}result and light up on user-declared directives:Hovers.computesurfaces directive-name hovers (pre-coordinate branch ondirective.nameNode()) and arg-name docstring fallback fromInputValueShape.description();Diagnostics.computeextends its existing outer-snapshot / inner-resolution switch onBuilt.CurrentwithvalidateUnknownArgsAgainstSnapshot+validateRequiredArgsAgainstSnapshotpackage-private helpers next to the bundled equivalents;ArgNameCompletions.generategrows anLspSchemaSnapshotparameter and routes the User arm through a top-level-onlyuserGeneratehelper (nested completion stays empty until the snapshot carries input-object shapes). Hovers and completions are freshness-agnostic (stale info beats silence); diagnostics warn only underBuilt.Current(mirrors R139’s unknown-directive arm). Bundled-shadows-snapshot precedence (R139 settled design note 4) pinned with parallel guards on all three consumers:Hoversgates user-arm fallback onresolution instanceof DirectiveResolution.User,Diagnosticskeeps the existingBundledearly-continue, and the newbundledDirectiveArgHover_ignoresSnapshotShadow/bundledDirectiveShadowedBySnapshot_routesThroughBundledPathcases anchor the guard symmetrically withDiagnosticsTest.bundledArgValidationStillFires_evenWhenSnapshotShadows. Tests. New pipeline cases:HoversTest(6 ; directive-name, arg-name, Unavailable/Previous freshness, shadow guard, bundled side-benefit),DiagnosticsTest(6 ; unknown-arg, missing-required, present-required-silent, Unavailable/Previous silence, shadow guard),ArgNameCompletionsTest(5 ; top-level snapshot args, nested-deferred-empty, Unavailable empty, Previous still emits, shadow guard). Existing bundled-path tests stay unchanged; the snapshot parameter threadsLspSchemaSnapshot.unavailable()for tests not exercising the user-arm. No unit-tier additions (each consumer is a thin walker over the recordsCatalogBuilderSnapshotTestalready pins; seal exhaustiveness isjavac-checked); no schema-fixture additions tographitron-sakila-example(R139’s@auth(role: String!)fixture remains the regression guard for the input contract); no execution-tier (LSP behaviour is observable in pipeline outputs). Audit unchanged. No new@DependsOnClassifierCheckmarkers ;Hovers.computeandArgNameCompletions.generateare freshness-agnostic so no classifier guarantee is load-bearing for them, andDiagnostics.compute’s existing marker still covers the new arg-validation arms (they inherit the same `Built means clean parsedependency through the samecomputebody). The R139 prep-note’s "two more markers" expectation was wrong in spirit; the audit-widening decision stays tracked under R139’s "Future evolution". Deferred (called out in the spec body, unchanged): project user-declared input-object types into the snapshot (lights up nested unknown-field validation inDiagnosticsand nested arg-name completion inArgNameCompletionstogether as a producer-side widening); lift the directive-name hover branch intoLspVocabularyonce a third consumer wants it. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R138 (
a5bc310In Progress → In Review implementation;bb415a2plan-body self-review addendum): Extend Invariant #15 to the Payload arm. The pre-R138 classifier admitted bulk-input + single-record-payload DML mutations (e.g.createFilmsPayload(in: [FilmCreateInput!]!): FilmPayload) via R75 Phase 1’sNoBacking-promotion carrier path, bypassing the out-of-band deferred rejection inFieldBuilder.buildDmlField; the generated fetcher ended invaluesOfRows(…).returningResult(…).fetchOne(), which throwsTooManyRowsExceptionfor every input with >1 row. Fix lifts thelistInput && !returnType.wrapper().isList()predicate toMutationInputResolver.validateReturnType’s sealed-root level so it fires uniformly across all three admitted return-type arms (`ScalarReturnType(ID),TableBoundReturnType,ResultReturnType), retires the duplicated per-arm check on the ID and T arms, and rewrites the rejection message to nameTooManyRowsExceptionas the runtime failure (replacing the pre-R134 "silent drop of all-but-last-row data" framing).FieldBuilder.buildDmlField’s deferred-rejection block is deleted along with its now-unused `listInputparameter and the four kind-switch call sites.TypeFetcherGenerator.buildMutationDmlRecordFetchercollapses to a single emit shape: theif (tia.list())empty-list short-circuit block, thedataIsListlocal, thepayloadTypeternary, and the.fetch()/.fetchOne()terminator ternary all retire; the fetcher emits a singlerowType RecordN<…>via unconditional.fetchOne(), and the Javadoc is rewritten to describe the one remaining shape.graphitron-sakila-example/schema.graphqlsdrops thecreateFilmsPayloaddeclaration and its R134 explanatory comment block (the shape is now unclassifiable, so R134’s compilation-tier regression has no surviving anchor ; the principled outcome, not a coverage gap). Tests. NewGraphitronSchemaBuilderTest.DML_INSERT_LIST_PLAIN_PAYLOAD_REJECTEDrow covers the plain-SDL carrier variant; the pre-existingDML_INSERT_LIST_PAYLOAD_DEFERREDrow renames toDML_INSERT_LIST_PAYLOAD_REJECTEDand retargets its assertion to"must return a list"+"Invariant #15"(both@record-carrier and plain-SDL variants now land at the same validator decision).MUTATION_DML_RECORD_FIELDflips fromcreateFilms(in: [FilmCreateInput!]!)tocreateFilm(in: FilmCreateInput!); fourSingleRecordCarrierPipelineTestfixtures (carrier_listDataField_classifiesAsMutationDmlRecordField,carrier_listDataField_dataFieldClassifiesAsSingleRecordTableField,carrier_atRecordWithNullClassName_classifiesAsMutationDmlRecordField,carrier_withDelete_rejectsAtClassifier) switch frompayloadDml(bulk) topayloadDmlSingleInput; the carrier-promotion and trigger-rejection cases keep bulk input because the per-arm rejection fires first. No execution-tier test (the failure mode isTooManyRowsExceptionthrown insidetransactionResult; an "asserts throws" test carries no signal beyond classifier rejection). Defers the bulk-carrier-with-list-data-field permit (MutationBulkDmlRecordField-style sealed leaf,Result<…>-keyed response-SELECT, list-element data-field classifier) to a future Backlog item under the slugbulk-input-single-carrier-list-data-fieldif a real schema surfaces a need. Build green:mvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R130 (
57d6673Phases 1–4 implementation;8f42848Phase 4 compile- and execute-tier coverage): Admit the two same-table@nodeId-decoded input-field carriers (InputField.ColumnFieldwithCallSiteExtraction.NodeIdDecodeKeysandInputField.CompositeColumnField) in@mutationinputs and@lookupKeybindings. The headline forcing function is composite-PK DELETE shaped likeslettRegelverksamling(input: { id: ID! @nodeId @lookupKey })against a composite-PK table; post-R131 the same-table arm classifies asCompositeColumnFieldand pre-R130 theMutationInputResolverrejected the carrier outright. Phase 1 (model + extraction-propagation fix): newInputColumnBindingGroupsealed root (MapGroup,DecodedRecordGroup) as a sibling to R50’sLookupArg, rooted at an input-field cluster rather than an outer GraphQL argument;InputFieldgains sealedLookupKeyField/SetFieldsub-interfaces permittingColumnFieldandCompositeColumnFieldonly (reference carriers stay outside the permits set);TableInputArg.fieldBindingsretypes toList<InputColumnBindingGroup>andlookupKeyFields/setFieldsretype to the new sealed permits;EnumMappingResolver.buildLookupBindingshonors the carrier’scf.extraction()when non-Directinstead of unconditionally re-deriving from raw column metadata. The pre-R130 unconditional re-derivation discarded the resolver-suppliedNodeIdDecodeKeys; the R131 follow-up SDL-boundary@nodeIdguard (lookup-key-input-field-non-nodeid-decoded) papered over the bug at the cost of rejecting the shape entirely. The fix at source retires that key and replaces it with two new@LoadBearingClassifierCheckkeys (mutation-input.lookup-binding-honors-carrier-extraction,mutation-input.lookup-binding-decoded-record-arity-matches-carrier-columns) paired with@DependsOnClassifierCheckconsumers on the lookup-WHERE / row-IN / INSERT-arm emitters. Phase 2 (classifier admission):MutationInputResolveradmitsColumnField(NodeIdDecodeKeys)andCompositeColumnFieldin lookup-bearing verbs; reference-carrier rejections reframe as R24-shapedRejection.deferred(summary, "nodeidreferencefield-join-projection-form");CompositeColumnField × INSERTcarves out viaRejection.deferred(summary, "")(no roadmap item exists today; lifts when a forcing-function schema appears);CompositeColumnFieldoutside@lookupKeyposition on UPDATE / UPSERT also rejects (the SET-side / INSERT-arm dispatch for composite-PK column writes is out of R130 scope). Phase 3 (emitter dispatch):buildLookupWhereSingleRowlifts a per-rowRecord<N>decode local topostInGuardwithThrowOnMismatchnull handling (GraphqlErrorException on wrong-type id);buildBulkLookupRowInadopts a block-lambda form for decode-bearing groups (expression-lambda preserved for the all-Directshape so existing pipeline traces stay byte-identical); INSERT / UPSERT column lists expandCompositeColumnFieldinto its N member columns viabuildInsertColumnList;buildPerCellValueListdispatches on carrier identity for the values list;buildInsertDecodeLocalslifts decode locals intopreGuardfor single-row INSERT / UPSERT (per-row inside the stream lambda for bulk). Phase 4 (tests): classifier tests inMutationDmlNodeIdClassificationTestfor composite-PK DELETE / UPDATE / UPSERT admission, INSERT carve-out, and single-PK extraction-propagation; the two R131 follow-upGraphitronSchemaBuilderTest.ArgumentParsingCase.LOOKUP_KEY_ON_NODEID_INPUT_FIELD_REJECTED{,COMPOSITE_PK}cases retype toLOOKUP_KEY_ON_NODEID_INPUT_FIELD_ADMITTED{,_COMPOSITE_PK}assertingMapInput.bindings[0].extractionisNodeIdDecodeKeys(single-PK) andLookupArg.DecodedRecord.bindingshas the expected positional arity (composite-PK). Phase 4 (compile + execute) (8f42848): sakila-example surfacesDeleteFilmActorByNodeIdInput @table(name: "film_actor")withid: ID! @nodeId(typeName: "FilmActor") @lookupKeyplusMutation.deleteFilmActorByNodeIdandMutation.deleteFilmActorsByNodeIddriving bothbuildLookupWhereSingleRow’s `DecodedRecordGrouparm andbuildBulkLookupRowIn’s block-lambda arm end-to-end; new `keyed_node(id varchar PK, label varchar)table ininit.sqlplusKeyedNode @nodetype andMutation.createKeyedNodedriving theColumnField(NodeIdDecodeKeys)INSERT-arm (buildInsertDecodeLocalspreGuard local +buildPerCellValueListNodeIdDecodeKeysarm); five execution-tier tests inDmlBulkMutationsExecutionTestcovering composite-PK DELETE single-row, composite-PK DELETE bulk row-IN, single-PK INSERT round-trip, andThrowOnMismatchon both the lookup-key and INSERT-arm paths. Deferred (acknowledged scope reduction, not rework): composite-PK UPDATE / UPSERT execution-tier proofs (the single-row decode-local lift is shared verb-agnostically across DELETE / UPDATE / UPSERT; classifier-tier admission is pinned inMutationDmlNodeIdClassificationTest.compositePkNodeIdLookupKey{update,upsert}_admitted); reference-carrier admission stays R24-coupled (no forcing-function schema today). Retired key:lookup-key-input-field-non-nodeid-decodedretires producer-only (zero@DependsOnClassifierCheckconsumers;LoadBearingGuaranteeAuditTestsurfaces no orphan). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; 23 tests inDmlBulkMutationsExecutionTestpass, 10 inMutationDmlNodeIdClassificationTestpass. -
R75 (
dbffee9Phase 1 reshape;862bc86Phase 1 direct-@tabletwo-step emit + durability pins;2fd7598Phase 1 follow-up SELECT coverage;88df99aPhase 2 foundation;be26134Phase 2 lean + R137 carve-out;2408634+031c6f3Phase 2 review fixes): Plain payload types for DML mutations. Replaces the earlier wire-format-unwrap design (PassthroughDataFieldpermit +IdentityPassthroughcapability +BuildContext.resolveReturnTypeshort-circuit, all retired) with the structural model the SDL implies: plain SDL Object carriers promote to a newPojoResultType.NoBackingarm at type-classification time, payload-returning DML mutations classify asMutationField.MutationDmlRecordField(DELETE rejected at classify time via the compact constructor), and the data field on the carrier classifies as the newChildField.SingleRecordTableFieldsibling permit with an inlineSourceKey(newReader.ResultRowWalkpermit on R38’s sealedReaderinterface,Wrap.Record, empty path, PK columns from the input@table; cardinality from the data field’s wrapper). DML emit becomes two-step uniformly across carrier and direct-@tableshapes: PK-onlyRETURNINGinsidedsl.transactionResult(tx → DSL.using(tx)….), then a follow-up SELECT outside the transaction lambda ; field errors during traversal cannot undo the DML. Phase 2 (lean) widens the trigger to admit record-backedResultTypeelements via a sealedDataElementsub-taxonomy (Table/Record); record-element data on@servicemutations classifies as the newChildField.SingleRecordIdentityFieldpermit (identity-passthrough emit, noSourceKey, no SELECT), and DML mutations reject record-element carriers at classify time. Audit. Two new@LoadBearingClassifierCheckkeys (mutation-dml-record-field.data-table-equals-input-tableandsource-key.result-row-walk-wrap-record-empty-path) pair with consumers on the mutation-fetcher RETURNING emit and the data-field response-SELECT emit. Tests. Pipeline-tierSingleRecordCarrierPipelineTestcovers per-DmlKindadmission, fullSourceKeyshape,PojoResultTypesplit, DELETE rejection, trigger rejections, table-equality rejection, the structural two-step-emit pin on direct-@tablereturns, the fetcher-emitter arm-count pin, Phase 2’s record-element classification, the parameterised record-element DML rejection (INSERT/UPDATE/UPSERT), and theSingleRecordIdentityFieldarm pin. Execution-tierSingleRecordCarrierDmlTestcovers round-trip for INSERT / UPDATE / UPSERT (new and existing rows) against sakila plus selection-set strength tests (auto-PK, DB default,@referenceprojection, post-UPDATE state read), plus the headline durability pinsdml_persists_when_followupSelect_throws(carrier) anddml_persists_when_directReturnSelect_throws(direct-@table) via the syntheticDurabilityErrorService.synthesizemid-traversal throw.GraphitronSchemaBuilderTest.NonTableParentCasegainsSINGLE_RECORD_CARRIER_DATA_FIELD,SINGLE_RECORD_IDENTITY_FIELD, andMUTATION_DML_RECORD_FIELD. Compilation-tier sakila fixtureMutation.createFilmCard(filmId: Int!): SingleFilmCardCarrierwires the@service-mutation +NoBacking-carrier + record-element-data-field shape end-to-end. Carved out to R137 (service-wrapper-composition): the 8-case execution matrix over{T, Optional, CompletableFuture, Mono, DataFetcherResult} × {Table, Record}and the data-element-aware strict service-return validator, both blocked on the@servicesubstrate admitting wrapper layers on method return types. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 across all 11 modules. -
R131 (
a64cd8f+1acbaa0+fe2de55+beb0e92): Collapse the singularid: ID! @nodeIdinput-field classifier ontoNodeIdLeafResolver.resolveso both arities (ID!and[ID!]) consume the same sealedResolvedoutcome (SameTable | FkTarget.DirectFk | FkTarget.TranslatedFk | Rejected) through a single shared helperBuildContext.inputFieldFromNodeIdResolved. Pre-R131 the singular branch open-coded typeName inference, schema/catalog lookup, path parse,validateLift, andliftSourceColumns, then funneled every outcome into the Reference-onlybuildInputNodeIdReferencesink ; even on the canonical same-table case where the leaf semantically filters the parent’s own rows by primary key. The reproducer schema (SlettRegelverksamlingInput @table(name: "regelverksamling") { id: ID! @nodeId }on a composite-PK table) now lands onCompositeColumnField(same-table arm), notCompositeColumnReferenceField. The duplicateBuildContext.NodeIdTypeNameInferencerecord andinferNodeIdTypeNamehelper retire ;NodeIdLeafResolver.inferTypeNameis the single home.buildInputNodeIdReferencesurvives only as the id-reference synthesis shim’s sink and is documented as such. Audit in the same commit: producer@LoadBearingClassifierCheck("nodeid-fk.direct-fk-keys-match")and consumer@DependsOnClassifierCheckannotations onBuildContext.classifyInputFieldInternalandFieldBuilder.walkInputFieldConditionsrewritten to describe the post-R131 shape; the stale "`CompositeColumnReferenceField` may represent a same-table PK filter" reading is gone (MutationInputResolverrejects all four@nodeId-decoded input-field carriers as deferred; R130’s post-R131 pivot retains scope for the same-table column-direct half ;ColumnFieldwithNodeIdDecodeKeysandCompositeColumnField, the carriers the post-R131 classifier produces fromSlettRegelverksamlingInput-shaped schemas ; and defers the genuinely-joinedReferenceFieldhalf per R24’s "wait for forcing-function schema" discipline). *Tests:NodeIdPipelineTestfour-corner pipeline pins for singular + (same-table |FkTarget.DirectFk) × (single-PK | composite-PK) ;InputCase.EXPLICIT_NODE_ID_DIRECTIVEupdated toCompositeColumnField, newEXPLICIT_NODE_ID_DIRECTIVE_SINGLE_PK→ColumnField, newInputReferenceCase.REFERENCE_TO_COMPOSITE_PK_NODE_TYPE→CompositeColumnReferenceFieldwith positionally-alignedliftedSourceColumns,NODE_TARGET_NO_METADATA_PK_FALLBACKupdated toColumnField, newArgumentSameTableNodeIdCase.SAME_TABLE_SCALAR_COMPOSITE_PKpinningFieldBuilder.classifyArgumentemitsBodyParam.RowEqover the parent’s PK columns;MutationDmlNodeIdClassificationTest.nodeIdFieldInInput_deferredrejection text shifts toCompositeColumnField. Compilation tier (sakila example): newFilmActorSingularNodeIdFilter @table(name: "film_actor") { id: ID! @nodeId(typeName: "FilmActor") }plusQuery.filmActorBySingularCompositeNodeId(filter:)emitsDSL.row(table.ACTOR_ID, table.FILM_ID).eq(id).CompositeDecodeHelperRegistry.buildHelperdrive-by switches the singular non-list branch fromvar r = …to a typedRecord<N>declaration via a newtypedRecordhelper (caught byGeneratedSourcesLintTest.varGuardonce the singular composite-PK path reached this branch for the first time). Reachability claims (multi-hop, condition-step, andTranslatedFkrejections) hold no-op-by-construction via the shared route; pipeline tests anchor on the resolver’s shared marker constants (LIFT_FAILURE_MARKER,CONDITION_STEP_MARKER), not on copied substrings. Follow-up1acbaa0(FK-target / NodeType-keyColumns permutation):NodeIdLeafResolver.permutationToKeyColumnsreplaces the strictsameColumnsBySqlNamepredicate ; when the terminal hop’s target-side columns equal the NodeType’s@node(keyColumns:)as a multiset (any order), theDirectFkarm permutesjoinPath.liftedSourceColumns()intokeyColumnsorder before constructing the carrier; the@LoadBearingClassifierCheckdescription rewrites "positionally match" to "equal as a multiset, in any order" and tightens the carrier guarantee. Pinned byInputFieldFkTargetNodeIdCase.FK_TARGET_REORDERED_KEY_PERMUTATION_DIRECT_FK{,_SINGULAR}over the newreordered_pk_parent/reordered_fk_childfixture (declared FK target order(pk_b, pk_c, pk_a), NodeType keyColumns[pk_a, pk_b, pk_c]). Resolves a latent zero-rows regression in the downstreamopptak-subgraphregelverksamlingIdschema. Follow-upfe2de55+beb0e92(@lookupKeycomposition guard): post-R131 the singular same-table@nodeIdcarrier isInputField.ColumnField(orCompositeColumnField), soEnumMappingResolver.buildLookupBindings’s pre-R131 structural rejection no longer fires; `beb0e92moves the guard upstream tosdlField.hasAppliedDirective(DIR_NODE_ID)so both arities surface the same diagnostic ("expose the decoded key column(s) explicitly via@fieldinstead, or move@lookupKeyto the outer argument"). NewLoadBearingClassifierCheckkeylookup-key-input-field-non-nodeid-decoded, new test casesGraphitronSchemaBuilderTest.ArgumentParsingCase.LOOKUP_KEY_ON_NODEID_INPUT_FIELD_REJECTED{,_COMPOSITE_PK}.NodeIdLeafResolver.validateLift/liftSourceColumnstightened from package-private to private (no external callers after the routing collapse). Two follow-on Backlog stubs filed for material gaps surfaced in self-review:R135(multi-hop@nodeIdpermutation pipeline test, to prove the multi-hop case the commit asserts works by construction) andR136(execution-tier round-trip for the FK-permutation case viagraphitron-sakila-example+GraphQLQueryTest). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25, 1900+ tests passing. -
R42 (
506c704+5d90719): LiftChildField.ColumnReferenceFieldout ofTypeFetcherGenerator.STUBBED_VARIANTSfor theCallSiteCompaction.Direct+ FK-only path. NewInlineColumnReferenceFieldEmitterbuilds the$fieldsswitch-arm body as a single-column correlated subquery (DSL.field(DSL.select(<terminalAlias>.<COL>).from(<terminalAlias>).join(…).where(<correlation>).limit(1)).as("<fieldName>")), mirroringInlineTableFieldEmitter’s shape collapsed to a scalar (`DSL.field, notDSL.multiset).TypeClassGenerator.$fieldsgains acase ChildField.ColumnReferenceField crfarm;TypeFetcherGeneratormoves the leaf fromSTUBBED_VARIANTSintoPROJECTED_LEAVESand the fetcher switch arm becomes a no-op.FetcherEmitterwiresnew ColumnFetcher<>(DSL.field("<name>"))for the Direct shape. The two non-lifted shapes surface at build time, not runtime:GraphitronSchemaValidator.validateColumnReferenceFieldrejectsNodeIdEncodeKeyswithRejection.Deferredkeyed tonodeidreferencefield-join-projection-form(R24) and anyJoinStep.ConditionJoin-in-path withRejection.Deferredkeyed to the newly-allocated R129 (column-reference-on-scalar-field-condition-join). The validator/emitter contract carries the@LoadBearingClassifierCheck/@DependsOnClassifierCheckannotation pair (keyscolumn-reference-field-no-nodeid-encode-keysandcolumn-reference-field-no-condition-join-step), soLoadBearingGuaranteeAuditTestwalks the dependency and the FetcherEmitterNodeIdEncodeKeysruntime stub forColumnReferenceFieldretires as defence-in-depth without an annotated guarantee (CompositeColumnReferenceField’s parallel arm is unaffected; R24 still owns it). Tests: `ColumnReferenceFieldValidationTestrestructured for the four-shape matrix (Direct + FK-only passes; Direct + ConditionJoin and NodeIdEncodeKeys + FK-only get the deferred messages; empty path keeps the structural "path is required" error);NestingFieldValidationTeststubbed-nested cases switched toCompositeColumnReferenceField(which remains stubbed); new pipeline-tierColumnReferenceFieldPipelineTestcovers single-hop and multi-hop projection plusColumnFetcherwiring; sakila-example schema addsFilm.languageName: String @field(name: "NAME") @reference(path: [{key: "film_language_id_fkey"}]);GraphQLQueryTest.films_languageName_resolvesViaScalarReferencecovers the execution tier against PostgreSQL. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R43 (scoping change, this commit): The scalar/enum-return form of
@tableMethod(originally tracked as the carve-out undertablemethod-scalar-return.md) is closed by rejecting the shape at classification rather than implementing it.TableMethodDirectiveResolvernow rejects any non-TableBoundReturnTypereturn as a structural schema error ("@tableMethod requires a @table-annotated return type") at both root and child sites; the previous gating onisRootis gone, and the resolver’s sealedResolvedcollapses to{TableBound, Rejected}(theNonTableBoundarm was the only producer ofTableMethodFieldwith a non-table return and is dead code now).ChildField.TableMethodField.returnType()is tightened fromReturnTypeReftoReturnTypeRef.TableBoundReturnTypeto express the classifier guarantee in the model.TypeFetcherGenerator.STUBBED_VARIANTSkeeps theTableMethodFieldentry with reworded summary ("child @tableMethod (table-bound return) not yet implemented") and a renamed planSlugtablemethod-child-table-bound; the roadmap file is renamed to match and the item body now scopes R43 narrowly to the table-bound child case (QueryField.QueryTableMethodTableFieldat the root already ships inIMPLEMENTED_LEAVES). Pipeline tests inGraphitronSchemaBuilderTest.UnclassifiedFieldCasegain two cases (TABLEMETHOD_AT_ROOT_WITH_SCALAR_RETURN_REJECTED,TABLEMETHOD_ON_CHILD_WITH_SCALAR_RETURN_REJECTED) asserting the exact rejection message on both sites;TableMethodFieldValidationTestfixtures stop constructing the variant withScalarReturnType(no longer reachable from real classification) and useTestFixtures.tableBoundFilminstead. Rationale:@tableMethod’s purpose is to bind a developer-authored jOOQ table method, which by construction returns a generated jOOQ table class. A scalar/enum return cannot be made to work; calling the shape "deferred" misled authors with a roadmap link to functionality that would never arrive. Build green: full `mvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R101 (
ef66e8bPhase 1;fbe354ePhase 2;46f08f8Phase 3; this commit Phase 4): Custom-scalar Java type configuration. The hardcoded five-site spec-built-in switch (ServiceCatalog.mapToJavaTypeName,FieldBuilder.mapGraphQLTypeToReflectType,RowsMethodShape.standardScalarJavaType,AppliedDirectiveEmitter.emitInputType,GraphitronSchemaClassGenerator’s literal `.additionalType(…)block) is retired in favour of a singleScalarTypeResolversource-of-truth carrying a sealedScalarResolution.{Resolved | Rejected}outcome. Consumers bind a custom scalar by pointing at apublic static final GraphQLScalarTypeconstant on the classpath, either by directive (scalar Money @scalarType(scalar: "com.example.Scalars.MONEY")) or by thegraphql-java-extended-scalarsconvention table (scalar BigDecimalresolves toExtendedScalars.GraphQLBigDecimalwhenever the artifact is on the consumer’s compile classpath, no directive needed). Graphitron reflects on the constant’sCoercing<I, O>parameters to recover the Java type, and emits.additionalType(…)automatically. Migration: consumers running on the rewrite must remove their manual.additionalType(ExtendedScalars.GraphQLBigDecimal)/.additionalType(consumerScalar)calls frombuildSchema(…)hooks for any scalar graphitron now resolves; graphql-java’sGraphQLSchema.Builder.additionalTyperejects duplicate type names at build time, so leaving the call in turns into aSchemaProblemrather than silent tolerance. Resolution order: spec built-ins (Int,Float,String,Boolean,ID) win;@scalarTypebeats the convention layer; convention lands when the SDL name matches an entry onScalarTypeResolver’s 30-entry table and `graphql.scalars.ExtendedScalarsis on the classpath. Unresolved → hard validation error pointing at@scalarType(scalar:)or extended-scalars as the fix; no silent fallback toObject. Phase 4 housekeeping: LSP completion on@scalarType(scalar: |)suggests convention-table FQNs (preferring the entry that matches the enclosing scalar’s SDL name); LSP diagnostics surface malformed-FQN and unknown-class cases inline against the catalog’s external-reference scan; newBehavior.ScalarTypeBindingarm on the@scalarType(scalar:)coordinate;Documentation/code-generation-triggers.adocgains a@scalarTyperow; the scalar resolution story is documented in the manual reference page. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R127 (
8310179+6308198): AcceptList<XRecord>as well asResult<XRecord>at root@serviceon a@table-bound list return. The classifier’s strict catalog-sideTypeName.equalscheck now returnsnullfromServiceDirectiveResolver.computeExpectedServiceReturnTypefor theTableBoundReturnType+ List arm, and a new resolver-sidevalidateRootListTableBoundReturnPairrejects any reflected method-return that isn’t exactlyorg.jooq.Result<XRecord>orjava.util.List<XRecord>(graphql-java treats both identically;Result extends List).TypeFetcherGenerator.buildQueryServiceTableFetcherand.buildMutationServiceTableFetcherreadMethodRef.returnType()for the List arm so the generated local declaration tracks whichever shape the developer chose. Single cardinality stays strict via the catalog. Annotations. A newLoadBearingClassifierCheckkeyservice-resolver-root-list-record-return-pairowns the resolver-side pair check; the existingservice-catalog-strict-service-returndescription narrows to the Single arm +ResultReturnTypepaths; both root emitters declare both keys via@DependsOnClassifierCheck. Tests.TestServiceStub.getFilmsAsListreturnsList<FilmRecord>;ServiceRootFetcherPipelineTestgains a positive case (serviceWithListOfRecordReturn_isAccepted) and a negative case (serviceWithWrongInnerGenericOnList_surfacesAsValidationErrorWithPairedShapes) asserting the rejection names both accepted shapes, the actual mismatched shape, and carries the"service method could not be resolved ; "prefix the Single-arm rejection wears.LoadBearingGuaranteeAuditTestcovers the new key automatically. Workflow note. The item was filed directlyIn Progress: the inbound was framed as an operational bug report and the agent began implementation before the Backlog → Spec → Ready gate;CLAUDE.mdwas tightened in the same branch so the next operational-looking inbound (stack trace plus "make it accept X") doesn’t slip past. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25, 1564 tests passing. -
R68 (
f5c75ef; Phases 1a–6 SHAs compressed by upstream rebase): Diataxis user manual absorbs the legacygraphitron-codegen-parent/graphitron-java-codegen/README.mdinto the deployed site. Five top-level chapters under/docs/manual/(index,tutorial,how-to,reference,explanation) mirror the Diataxis quadrants. Tutorial (six pages anchored tographitron-sakila-example) verified byTutorialSmokeTest; a@QuarkusTestinside the example module replaying each page’s HTTP query against the JAX-RS endpoint. Reference: 26 directive pages 1:1 againstdirectives.graphqls(drift-pinned byDirectiveDocCoverageTest);mojo-configuration.adocreflected from the Mojo’s@Parameter-annotated fields (MojoDocCoverageTest);diagnostics-glossary.adoccovering the 16-codeRejectionKind/AttemptKind/EmitBlockReasonclosed set (DiagnosticsDocCoverageTest);deprecations.adocextracted from SDL@deprecated()markers with@indexallow-listed for the GraphQL-spec-disallowed whole-directive case (DeprecationsDocCoverageTest); plusruntime-api.adocandspecial-interfaces.adocas hand-curated prose. How-to: 14 recipe-shaped pages with "verified by" pointers into thegraphitron-sakila-example/src/test/java/…/querydb/consumer test surface (includingtest-your-schema.adoc, net-new prose with no legacy precedent). Explanation: six pages (why-database-first,why-jooq-and-graphql-java,how-it-works,classifier-mental-model,batching-model,design-decisions). Cutover (Phase 6):docs/quick-start.adoc:15flipped from the legacy GitHub README pointer to the in-treexref:manual/reference/directives/index.adoc. Rework pass (f5c75ef): cleared fourR<n>leaks from user-facing prose flagged by the In Review reviewer ;R47reference inexternal-code.adoc:116dropped,R114inmulti-hop-nodeid-filter.adoc:15rephrased to feature-by-name, theuntil R61historical-Invariant bullet inresult-types.adoc:142dropped entirely, and theR75:Javadoc prefix onPassthroughDataFieldinChildField.java:361stripped at source (the migration fragment androadmap/inference-axis-coverage.adocregenerated). The Phase 1a–6 implementation SHAs the spec body cited (3afc278,fa36dbc,d0c63c4,8f7d412,1ea0855,868593a,d796c4c,3f6ec55,23c2056,863d8be) no longer resolve in local history ; compressed by an upstream rebase before the rework cycle. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25 with 312 + 25 tests passing, all five drift-protection verifiers green. -
R44 (
b978e69): Deprecate@multitableReference, mirroring the@notGeneratedremoval end-to-end. The directive stays SDL-declared indirectives.graphqlsso consumer schemas still parse, butFieldBuilder.classifyFieldnow rejects every application with anUnclassifiedFieldcarrying aRejection.directiveConflict(List.of(DIR_MULTITABLE_REFERENCE), "@multitableReference is no longer supported. Remove the directive; the rewrite generates multi-table interface dispatch from @discriminate / @discriminator without an explicit multitable-reference path."). The rejection is ordered abovedetectChildFieldConflictso the deprecation message wins over a mutual-exclusivity reason when the field also carries a conflicting directive (the load-bearing ordering invariant the spec called out, mirroring@notGenerated’s precedent). Model. `ChildField.MultitableReferenceFieldrecord and itspermitsentry deleted; sealed-switch exhaustiveness propagates the removal toGraphitronSchemaValidator(dispatch arm +validateMultitableReferenceFieldmethod gone) andTypeFetcherGenerator(STUBBED_VARIANTSentry + dispatch arm gone, with the[deferred] multitable-reference-on-scalarslug retiring as a dead anchor).detectChildFieldConflict’s mutual-exclusivity slot list drops `DIR_MULTITABLE_REFERENCEas dead vocabulary; theBuildContext.DIR_MULTITABLE_REFERENCEconstant, itsPASSTHROUGH_FORBIDDEN_DATA_FIELD_DIRECTIVESmembership, and theSchemaDirectiveRegistry.GENERATOR_ONLY_DIRECTIVESentry all retained per the deprecated-but-membership-retained precedent (the SDL declaration is still present). Tests. Pipeline-tierMultitableReferenceFieldCaserewritten from "produces aMultitableReferenceField`" to two cases: `REJECTEDassertsUnclassifiedFieldwith the deprecation reason, andREJECTED_WINS_OVER_CONFLICTpairs@multitableReferencewith@serviceand asserts the deprecation reason wins over the mutual-exclusivity reason (locking the ordering invariant). The redundantMULTITABLE_REFERENCE_AND_SERVICE_CONFLICTcase retires from the child-field conflict suite;MultitableReferenceFieldValidationTestdeleted outright. Docs.directives.graphqlsdescription rewritten in the@notGeneratedremoval shape;docs/manual/reference/directives/multitableReference.adocrewritten as a deprecation page (opener, SDL signature, Migration, Diagnostic, Constraints, See also); new=== @multitableReferencesubsection under "Hard removals" inmigrating-from-legacy.adoc; cross-references to@multitableReferenceretargeted to@discriminate/@discriminatorinreference.adoc,join-with-references.adoc,polymorphic-types.adoc,notGenerated.adoc, and the directive indices (entry moved to the "Rejected by the rewrite" category, gains the(rejected, remove from the schema)annotation); dispatch table row incode-generation-triggers.adocrewritten in the@notGeneratedrow’s shape. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R124 (
7882caf+f8d5300): Source the codegen reflection classpath from the project’s compile classpath + reactor siblingtarget/classes, not the plugin realm.AbstractRewriteMojo.withCodegenScopebuilds aURLClassLoaderoverproject.getCompileClasspathElements()plus the existingresolveClasspathRoots()set, parented on the plugin loader; the loader is threaded through a newRewriteContext.codegenLoaderfield to the 22 in-processClass.forName(name, false, loader)sites and also installed as TCCL for the duration of the scope (defense-in-depth for third-party transitive callees), restored infinally, and closed to release JAR file descriptors (matters forDevMojo’s per-cycle regeneration). The lone `DataFetchingEnvironmentreflection inClassAccessorResolverstays plugin-internal. API surface:RewriteContextgains a non-nullcodegenLoaderfield (eight-arg compact + seven-arg + six-arg back-compat overloads default it to TCCL for unit-tier callers);BuildContext.codegenLoader()is a thin passthrough mirroringnodeIdLeafResolver()andBuildContext.ctxis now@NonNull-enforced viaObjects.requireNonNull(the three unit-tier tests that previously passed(null, _, null)now construct a deterministic stub via the 6-arg overload);JooqCatalogtakes a(String, ClassLoader)constructor with a one-arg TCCL-defaulting back-compat overload;CheckedExceptionMatcher.unmatched/coversandServiceCatalog.argExtractiongain aClassLoaderparameter;TypeBuilder.validateExceptionClassandFieldBuilder.checkDeclaredCheckedExceptionsflip fromstaticto instance (single same-class callers; the explicit-parameter sibling lives where it crosses a class boundary). Migration:<plugin><dependencies>blocks deleted fromgraphitron-sakila-example/pom.xmland thebasic-generateIT pom; the IT now declaresgraphitron-sakila-dbas a normal top-level<dependency>, locking the contract in the IT itself. Tests: new pipeline-tierCodegenLoaderTeststages a hand-rolled.classfile (Java 17 encoding inlined asbyte[], so the test does not need a compiler on its own classpath) under a faketarget/classesdirectory, wires its path throughproject.getCompileClasspathElements(), and asserts (a) the staged class is not on the test JVM’s classpath, (b) insidewithCodegenScopethectx.codegenLoader()resolves it, (c) the TCCL inside the scope is the codegen loader, (d) the previous TCCL is restored after. The compile- and execution-tier load-bearing migration test is thegraphitron-sakila-examplereactor build, which now compiles and runs against the live schema with no<plugin><dependencies>block. Docs: new "Codegen classpath" section indocs/manual/reference/mojo-configuration.adocnames the new contract and the rare legitimate<plugin><dependencies>case (pinning a different version through the parent chain). Architect-review tightening (f8d5300) added a load-bearing comment toDevMojo.executeexplaining why the capturedinitialCtxmust only be read for path-shaped fields (its loader is closed by the time setup proceeds), and one-line policy notes on the two instance-method helpers explaining why they’re notstaticand why the cross-class siblings take an explicitClassLoaderparameter instead. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
R83 (
b3c5b6c+1187fa5): Pipeline + compilation + execution tier coverage for the multi-schema jOOQ fixture R78 introduced. Three independent tiers ride themultischemafixturecatalog (multischema_a+multischema_b, the cross-schema FKgadget → widget, the collidingeventtable). Pipeline tier:MultiSchemaPipelineTest(annotated@PipelineTier, ingraphitron/src/test) drives a slim SDL throughGraphitronSchemaBuilderagainstjooqPackage=multischemafixtureand asserts at two typed surfaces. Model-level:TableRef.tableClass()segmentation for the unique-per-schema (Widget →multischema_a.tables.Widget), qualified (@table(name: "multischema_a.event")→multischema_a.tables.Event), and cross-schema-FK-target (Gadget →multischema_b.tables.Gadget) resolution paths; cross-schemaJoinStep.FkJoin.fk().keysClass()routes tomultischema_b.Keys(the FK-holder schema, not the target’s schema ; the R78 bug case);firstHop.targetTable().tableClass()lands onmultischema_a.tables.Widget. Structural emit-side: a typed walk overTypeSpec.methodSpecs[].parameters[].type/returnType/fieldSpecs[].typeplus parsed imports from the renderedJavaFile, asserting everyClassNamewhose canonical name starts with the multischema-fixture root lives under amultischema_a/multischema_bsub-package ; the bare-root R78 bug shape (<jooqPackage>.tables.X,<jooqPackage>.Keys,<jooqPackage>.Tables) cannot appear anywhere typed-reachable from aTypeSpec. Targeted positive assertions read parameter types directly offQueryConditionsGeneratorandTypeClassGeneratoroutput. Compilation tier: thirdrewrite-generate-multischemagraphitron-maven-pluginexecution ingraphitron-sakila-example/pom.xmlconsuming a newsrc/main/resources/graphql/multischema.graphqls(the same three shape cases as the pipeline test) and writing to a disjointno.sikt.graphitron.generated.multischemaoutput package. The fullmvn install -Plocal-dbreactor compiles the multischema slice against the live multi-schema jOOQ catalog; a regression that re-derives aClassNamefrom the barejooqPackageemits source that does not exist (root.Keys/root.tables.Widgetnever resolve under multi-schema codegen) and the maven-compiler-plugin fails the build. Execution tier:MultiSchemaQueryTest(annotated@ExecutionTier, ingraphitron-sakila-example/src/test) loads the multischema slice’s ownGraphitron.buildSchema(…)and issues three queries against therewrite_testPostgreSQL ; the cross-schema FK round-trip (gadgets { gadgetId note widget { widgetId name } }), the schema-A unique resolution (widgets), and the qualified-form resolution (events). Seed rows added toinit.sqlfor one widget, two gadgets pointing at it, plus one row in each event collision table. Self-review pass (1187fa5): the principles-architect agent flagged that the originalb3c5b6crendered-text substring scans (assertThat(rendered).contains("…multischema_a.tables.Widget")plus a leading-space negative form to disambiguate root-truncated FQNs from correctly-segmented ones) were structurally indistinguishable from the body-content assertions banned at every tier. Replaced with the typed walk above; positive assertions now read parameter types directly (no string scan, no JavaPoet import-vs-inline coin-flip). The R78 invariants (TableRef.tableClass(),ForeignKeyRef.keysClass()) fit the@LoadBearingClassifierCheck/@DependsOnClassifierCheckpattern but the global annotation-pair sweep was deferred to R125 to keep R83’s scope on "make the multi-schema fixture earn its keep" rather than expanding into a broader principle-enforcement sweep. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25, all 11 reactor modules SUCCESS, both new test classes pass (7 pipeline tests + 3 execution tests, plus the R78 unit-tierJooqCatalogMultiSchemaTest). -
R86 (
a53502f+ede87f1): Architecture chapter for the user-manual site. Newtyped-rejection.adocconsolidates the sealedResolvednarrative across the thirteen*DirectiveResolversiblings, theRejectiontaxonomy (AuthorError.{UnknownName | Structural | AccessorMismatch},InvalidSchema.{DirectiveConflict | Structural},Deferred), and theBuildContext.candidateHintLevenshtein contract; D10 draws theRejectionsealed hierarchy withLookupKeyDirectiveResolver.Resolved.{Ok, Rejected}overlaid as a worked example.README.adocrewritten as an intent-routed chapter index with D1 (build pipeline).getting-started.adoc § Federationand§ Dev loopeach gain a marked=== How this is wired (for contributors)subsection (D9 federation entity flow, D7 dev-loop runtime framing).runtime-extension-points.adoc§ Where the interface comes from prepended with the per-app-emission rationale plus D4 (request lifecycle);code-generation-triggers.adoc§ Scope gains D3 (scope state machine). Four manual xrefs restored (explanation/index,classifier-mental-model,how-it-works,how-to/test-your-schema). NewSealedHierarchyDocCoverageTestwalksRejection.permits()transitively with bidirectional drift protection ; alternation built from the live permit set so future top-level branches extend coverage automatically.ManualXrefIntegrityTestextended to remap renderedarchitecture/paths back tographitron-rewrite/docs/(mirrors thestage-architectureblock’sREADME.adoc → index.adocrename) so source-tree resolution stays honest without staging.rewrite-design-principles.adocframing line and the Builder-step-results-are-sealed section collapsed to forward pointers intotyped-rejection.adoc. Two acknowledged deviations: wire-format-encoding principle stayed inrewrite-design-principles.adoc(page name and consolidation list are about typed rejection, not wire-format decode at the DataFetcher boundary), and existing tables incode-generation-triggers.adocweren’t converted to enriched form (read fine as-is). Build green; all five*DocCoverageTestsiblings pass. -
R119 (
531495a+6a644f1+698a21a+22dfb8a+54fc7c9+4ae827d+04daf62+cc5417c): LSP completion / diagnostics keyed by GraphQL schema coordinates. SealedSchemaCoordinate(Directive/DirectiveArg/InputType/InputField) plus sealedBehavioroverlay (ClassNameBinding/MethodNameBinding(classNameCoord)/CatalogTableBinding/CatalogColumnBinding/CatalogFkBinding/ArgMappingBinding) live in a newLspVocabularyrecord that wraps a parsedTypeDefinitionRegistryof the bundleddirectives.graphqls.LspVocabularyconstructor enforces the structural invariant ; every overlay coordinate must resolve against the registry ; and throwsLspStartupExceptionotherwise;DriftDetectionTestbuilds the production overlay against the real SDL so R110-style drift is a startup failure, not a silent unknown-directive at request time. SingleRewriteSchemaLoader.directivesSdl()accessor consolidates what was two private constants. Seven consumers (Diagnostics,Hovers,ClassNameCompletions,MethodCompletions,FieldCompletions,TableCompletions,ReferenceCompletions) migrate to behavior-arm dispatch viaLspVocabulary.coordinateAt(directive, pos, source)+behaviorAt(coord); the@sourceRowgap R110 left in place closes here as a side-effect of the unification. Five DX wins fall out of the parse: unknown-directive / unknown-arg / required-arg diagnostics (Warning severity) inDiagnostics, arg-name completion in a newArgNameCompletionsprovider chained last incoordinateBasedCompletions, and SDL-docstring fallback hover viaLspVocabulary.descriptionOf. Deletions:DirectiveDefinitions.java(124 lines),DeprecationMarkers.java(164 lines, two regex patterns),SdlAction.DeprecationTargetcollapses intoSchemaCoordinate.SdlActionDriftTestrewritten to read deprecations offLspVocabulary.deprecatedCoordinates()(native@deprecated(reason:)for member-level; docstring@deprecatedtoken-scan for whole-directive). New tests:LspVocabularyTest(10 cases pinning structural invariant + deprecation surface),CoordinateAtTest(cursor-to-coordinate across flat / single-level / multi-level nesting),ArgNameCompletionsTest(5 cases),DriftDetectionTest; consumer tests gain@sourceRowregression-guard cases. Self-review (04daf62) liftedsiblingStringAttoLspVocabulary(~120 duplicated lines retire fromMethodCompletions/Hovers/Diagnostics) and filed R123 for theMethodNameBinding+METHOD_VALIDATING_DIRECTIVESenclosing-directive-context smell that surfaced when the consumers stabilised. Findings noted at approval (non-blocking): (1)Diagnostics.METHOD_VALIDATING_DIRECTIVESis the renamed shadow of the previousVALIDATE_METHODset; the spec said this would be replaced byMethodNameBindingarms only being attached where method validation applies, but the structural redesign got deferred to R123 instead of landing here. (2)SdlActions.detectLegacyNameSitescallsLspVocabulary.load()on every detection (every code-action request re-parses the SDL + re-runs the structural-invariant loop); the workspace’s vocabulary instance is the once-only one, but theSdlAction.Detectorsignature doesn’t accept a vocabulary so per-request re-parsing is the only option without a contract change. Code-action requests aren’t on the keystroke hot path and the parse is small (~25 directives), so this is minor, but it contradicts the spec’s "shape, not state, read once" framing. (3) The L1 unitBehaviorTestper-binding-arm suite the spec called out didn’t land as a separate file; arm coverage is folded into the consumer tests, which keeps the assertion shapes but loses the tier-by-tier mapping. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25, all 11 reactor modules SUCCESS, 187 LSP tests pass. -
Discarded: collapse
BatchKeyFieldvalidator/emitter redundancy (collapse-tabletargetfield-redundancy, R4): superseded wholesale by R58 Phase G (commit3dcd3c6), which introduced the orthogonalConditionJoinReportablecapability and collapsed the fourunsupportedReasonoverloads inSplitRowsMethodEmitterto a single capability dispatch + the validator’s matching 4-arminstanceofchain to a singleinstanceof ConditionJoinReportablecheck. R4’s success criteria (validator 4 → 1 arms, emitter overloads gone, lock-step compiler-enforced) are all met in current trunk; the implementation diverged from R4’s literal proposal ; the predicate lives on a purpose-builtConditionJoinReportablerather than as a default onBatchKeyField, becauseServiceTableFieldis aBatchKeyFieldwithout the condition-join concern, so the narrower capability is the cleaner split. Item file deleted in this transition; the work itself shipped under R58. -
Surface silent
@splitQueryon@record-parent fields as a build warning (classification-vocabulary-followups, R3,17cc1a9+715a439):FieldBuilder.classifyChildFieldOnResultTypenow emits aBuildWarningviactx.addWarning(…)at both seams that head intoRecordTableField/RecordLookupTableField; the@sourceRowbranch (top of theif (DIR_SOURCE_ROW)block, beforeSourceRowDirectiveResolverruns) and the regular@record-parent branch (immediately afterresolveReturnTypeconfirmsReturnTypeRef.TableBoundReturnType, before path / table-field-components / batch-key rejection guards). Holistic surfacing: an unrelated rejection on the same field (bad lifter signature, unresolvable@reference, FK ambiguity) doesn’t suppress the redundancy advisory. Message names the field coordinate (<ParentType>.<fieldName>) and contains the substring"@splitQuery is redundant on a @record-parent field". Channel and prose form mirror the@table-shadowed-by-@recordprecedent atTypeBuilder.java:663; no new public API, no marker constant (deferred until R121’s LSP arm earns the second consumer). Closes the long-standing promise atcode-generation-triggers.adoc:105. Tests. Five pipeline-tier fixtures inGraphitronSchemaBuilderTest:SPLIT_QUERY_ON_RECORD_PARENT_WARNS_TABLE_FIELD,SPLIT_QUERY_ON_RECORD_PARENT_WARNS_LOOKUP_FIELD,SPLIT_QUERY_WARNS_ALONGSIDE_RECORD_PARENT_REJECTION(regular path; the last assertsUnclassifiedField+ warning);SPLIT_QUERY_WARNS_ON_SOURCE_ROW,SPLIT_QUERY_WARNS_ALONGSIDE_SOURCE_ROW_REJECTION(@sourceRowpath; the last assertsUnclassifiedFieldwithRejectionKind.AUTHOR_ERROR+ warning). Each asserts the classification arm viaisInstanceOf(…)plus the warning’s message-substring onschema.warnings(). Out of scope, follow-ups filed: LSP-tier diagnostic for the same warning (R121, where the marker constant earns its keep with a real second consumer);FkJoin.aliasdead-storage cleanup (R120); generalising theBuildWarningchannel into aWarningKindenum (no fourth producer to justify the lift); rejecting@splitQueryon@record-parent fields (the directive remains classified-but-no-op so existing schemas keep building). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
Add
Record1<T>source-shape support alongsideRow1<T>on the@serviceclassifier path (emit-record1-keys-instead-of-row1, R61,742f11bd+3d01c218+4ffdfc02+8fc61b95+bfeae318): developers choose either source shape at the@servicesource declaration ;Set<Row1<Integer>>(Row surface, novalue1()) orSet<Record1<Integer>>(Record surface, withvalue1()) ; and variant identity tracks the chosen shape so emit sites pattern-match instead of re-deriving. The classifier already routedList<Row<N>>/Set<Row<N>>toRowKeyed/MappedRowKeyedandList<Record<N>>/Set<Record<N>>toRecordKeyed/MappedRecordKeyed; this iteration made each variant’skeyElementType()andjavaTypeName()reflect the developer’s choice exactly and threaded that through the emit chain. Model.BatchKey.keyElementType()becomes the single source of truth via a default sealed switch on the root:RowKeyed/MappedRowKeyed/LifterRowKeyedproduceRowN<…>;RecordKeyed/MappedRecordKeyed/AccessorKeyedSingle/AccessorKeyedManyproduceRecordN<…>.javaTypeName()per-variant routes to a sharedcontainerType(container, shape, cols)helper. Variant rename.AccessorRowKeyed{Single,Many}→AccessorKeyed{Single,Many}(8fc61b95): theRowdiscriminator was leaking an emit-site detail (which jOOQ-typed local the framework picks for the projected key) into variant identity. There is no developer-supplied source on these arms ; the role is the lift-back into Graphitron scope after a@service/@externalFieldreturning aTableRecord; so the projection axis no longer encodes in the name. Used byServiceTableFieldandRecordTableField; the source-shape constraint lives in javadoc and is enforced byFieldBuilder.deriveBatchKeyFromTypedAccessor. Generators.GeneratorUtils.buildKeyExtraction(ParentKeyed) forks by variant identity:RowKeyed/MappedRowKeyedarms emitDSL.row(Record) env.getSource(.get(table.col), …);RecordKeyed/MappedRecordKeyedarms emitRecord) env.getSource(.into(table.col, …).buildKeyExtractionWithNullCheckstaysRowKeyed-only with a defensive IAE on misroute (single-cardinality@splitQueryon a@tableparent is the only caller).buildFkRowKey(RecordParentBatchKey RowKeyed) reads scalar values per parentResultType(jOOQTableRecord, jOOQRecord, Java record getter, typed POJO getter) and constructs theRowN<…>viaDSL.row(…). NewbuildAccessorKeySingle/buildAccessorKeyManyemit_elt.into(table.col1, …)to produceRecordN<…>keys, giving the auto-emitted rows-method’svalue<N>()access for the parent VALUES table emission. Parent VALUES emission.SplitRowsMethodEmitterforks two ways:RowN-keyed arms (RowKeyed,LifterRowKeyed) usek.field<N>()(returns the inline-valueFieldaDSL.row(value, …)-constructedRowcarries);RecordN-keyed accessor arms useDSL.val(k.value<N>())(extract the scalar; wrap as a bind-parameterFieldthat typechecks against the inline-ifirst arg of jOOQ’sDSL.rowoverload). Without theDSL.valwrap the column-referenceFieldrendered into the VALUES table at runtime instead of the value. Lift Invariant #10 (bfeae318): the validator’s single-cardinalityRecordTableField/RecordLookupTableFieldrejection (validateRecordParentSingleCardinalityRejected) was a downstream gate stranded by the rows-method router pinning the single-record-per-key arm toAccessorKeyedManyonly. The data-fetcher side (buildRecordBasedDataFetcher) already handled single cardinality cleanly via the(dispatch == LOAD_MANY || !isList) → RecordvalueType rule, so the only missing wiring was teachingemitsSingleRecordPerKey()to also be true for single-cardinality fields.RecordTableField.emitsSingleRecordPerKey()extends to!returnType().wrapper().isList() || batchKey() instanceof AccessorKeyedMany,RecordLookupTableFieldadds the missing override mirror, and the validator gate drops.@DependsOnClassifierCheckannotations. Two checks underbuildAccessorKeySingle/Manydescribing theField-typedinto(…)projection (accessor-rowkey-shape-resolvedfrom R60); paired with@LoadBearingClassifierCheckonFieldBuilder.deriveBatchKeyFromTypedAccessor. Tests. L1BatchKeyTestparameterised case pinskeyElementType()andjavaTypeName()per variant:RowKeyed,MappedRowKeyed,LifterRowKeyed→RowN<…>;RecordKeyed,MappedRecordKeyed,AccessorKeyedSingle,AccessorKeyedMany→RecordN<…>. L3ServiceFieldValidationTestadds dual-shape cells (MappedRowKeyed/MappedRecordKeyedboth classify cleanly on the same field). L3RecordTableFieldValidationTest/RecordLookupTableFieldValidationTestflip the threeSINGLE*cases from rejection to acceptance. L3GraphitronSchemaBuilderTestaddsRECORD_TABLE_FIELD_SINGLE_CARDINALITYpinning post-R61 acceptance +emitsSingleRecordPerKey()projection. L4TypeFetcherGeneratorTest.serviceField_mappedRecord_list_keyTypeIsRecordNpins theSet<Record1<Integer>>parameter shape andrecord.into(…)extraction. L5TestServiceStub.javakeeps bothRow1-source andRecord1-source fixtures (getFilmsWithSetOfRow1Sources/getFilmsWithSetOfRecord1Sourcessiblings) as the dual-shape coverage anchor. L6FilmService.titleUppercase(Set<Record1<Integer>>) → Map<Record1<Integer>, String>confirmsvalue1()works in the developer-side iteration; the existingRow1-source sibling confirmsfield1()-based dispatch keeps working. TheAccessorKeyedSingleexecution path is restored end-to-end against PostgreSQL (FilmCardData(FilmRecord film)+film: Film). Out of scope, deferred:@batchKeyLifterlifter return-type symmetry (the consumer-supplied static method still pinned toorg.jooq.Row1..Row22) ; owned by R71. Element-shape conversion forSet<TableRecord>/List<TableRecord>developer signatures ; closed by R70 by extending the variant taxonomy rather than threading conversion through the emitter. Open question closed in this iteration: "doesRow1afford a tuple-IN planner hint thatRecord1may not?" Resolution: no. jOOQ’sRecord1<T>extendsRow1<T>, so every typedRow1-API call site continues to type-check when handed aRecord1<T>; framework WHERE-clause emission reads keys viaRow-typed APIs and is shape-agnostic. Approval addendum: post-landing drift survives intact (this approval pass on 2026-05-09, ~5 days after In Review). The variant-identity-tracks-shape contract was the foundation two follow-on items explicitly built on. R70 (ea44908f) addedTableRecordKeyed/MappedTableRecordKeyedpermits to extend the cross-product to a third element shape (developer’s typedTableRecordsubtype);keyElementType()’s switch grew two cases, `buildKeyExtractiongrew a third arm emittingRecord) env.getSource(.into(Tables.X). R110 (75379091+3992f51+8922092+3b7f432) replaced@batchKeyLifterwith@sourceRowand splitLifterRowKeyedintoLifterLeafKeyed+LifterPathKeyedunder a newLifterKeyedsub-seal ofRecordParentBatchKey. R61’s row-vs-record symmetry survived: the lifter arms still produceRowN, the accessor arms still produceRecordN, and the deferred R71 surface (lifter return-type symmetry) remains the only consumer-supplied surface without shape-symmetry. R102, R77, R82, R78, R104, R114 all touched the variant taxonomy or its consumers without disturbing R61’s invariants. Editorial drift in the spec at approval (not blocking, file deleted on Done): the spec body retained pre-R110 names (LifterRowKeyed,@batchKeyLifter) at three call sites; this is purely cosmetic on the now-deleted artifact, and the implementation reflects the post-R110 shape correctly. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25; current trunk passes the same test surface that landed at In Review. -
Replace
@batchKeyLifterwith@sourceRowcomposing with@reference(sourcerow-replaces-batchkeylifter, R110,7537909+3992f51+8922092+3b7f432):@batchKeyLifteris removed;@sourceRow(className, method)replaces it with flat args (noExternalCodeReferencewrapper, notargetColumns) and composes with@referenceso multi-hop paths from a non-table-backed@recordparent become expressible. Model.BatchKey.LifterRowKeyedsplits intoLifterLeafKeyed(JoinStep.LiftedHop hop, LifterRef lifter)(no-@referenceleaf-PK arm) andLifterPathKeyed(List<JoinStep> path, LifterRef lifter)(@reference-composed FK chain) under a newBatchKey.LifterKeyedsub-seal ofRecordParentBatchKey.LifterPathKeyed’s compact constructor enforces non-empty path. The seal is honest about what it carries today ; the resolver’s typed return narrows to `LifterKeyed, andGeneratorUtils.buildRecordParentKeyExtraction’s exhaustive switch collapses both permits onto a single `case BatchKey.LifterKeyed lkarm because the lifter emit shape (Lifter.methodBacking) env.getSource() is identical for both ; and what it doesn’t (theSplitRowsMethodEmitterprelude consumes both shapes via theJoinStep.WithTargetcapability andRecordParentBatchKey.preludeKeyColumns(), not aLifterKeyed-typed parameter, so the capability-uniformity claim is future-facing rather than load-bearing in production). Resolver. NewSourceRowDirectiveResolverwith sealed builder-internalDerivation.{Leaf | Path}typing the parent-side tuple source:Leafreads the leaf target’s PK columns directly (single column-equality JOIN);Pathdelegates toBuildContext.parsePath(startSqlTableName=null)and takes the first FK hop’s source-side columns. Two diagnostic templates distinguish the two cases (per-position prose names "first-hop source-side column of FK '<fk>'" vs "primary key column '<col>' of '<leaf>'").@referenceparse failures surface directly without re-validating against the lifter. Reflection performs class load, single-static-method discovery, parameter-assignability check against the parent backing class, andorg.jooq.Row1..Row22raw-return + arity-bounds checks before the per-position erasure loop. Three classifier-check keys (@LoadBearingClassifierCheckon the resolver, paired with@DependsOnClassifierCheckon consumers):sourcerow-classifies-as-record-table-field(resolver always projects intoRecordTableFieldorRecordLookupTableField, paired withSplitRowsMethodEmitter.emitParentInputAndFkChain);sourcerow-leafkey-batchkey-is-lifterleafkeyedandsourcerow-pathkey-batchkey-is-lifterpathkeyed(the no-reference vs reference-composed permit guarantee, both paired withGeneratorUtils.buildLifterRowKey). Two keys not one because the relaxation surface is independent: a future variant could allowLifterKeyedoutside the leaf-PK / path-keyed split without affecting the other guarantee.LoadBearingGuaranteeAuditTestpicks up all three pairs automatically. Tests. L1 unitBatchKeyTestaddslifterPathKeyed_emptyPath_throwsIllegalArgument(compact-constructor invariant) and extendsrecordParentBatchKeyExhaustiveSwitchCompilesAcrossPermitsto verify the sub-seal compiles to a singleLifterKeyedarm. L4 pipelineGraphitronSchemaBuilderTest.SourceRowClassificationCase(renamed fromBatchKeyLifterCase) carries 16 cases covering: pojo +Row1<Integer>+@referencehappy path; lookup-key co-presence; null-fqClassName /@table/ jOOQ-record parent rejects;JavaRecordTypeadmit; missing class / missing method / multiple matches / wrong return / wrong param type rejects; arity / column-class / wildcard mismatches;@referenceparse failure passthrough;@asConnectionreject;@field(name:)non-interaction; field-level@conditionco-presence;@orderByco-presence; scalar return reject;LEAF_PK_NO_REFERENCE(the new no-@referencearm producesLifterLeafKeyed);LEAF_PK_ARITY_MISMATCH(leaf-PK diagnostic distinguishes from path-keyed). All 1465 graphitron tests pass. Sakila fixtures.CreateFilmPayload.languagemigrated to leaf-PK (@sourceRowalone, no@reference). New Story 1 fixture:Query.customerAddressSummary(customerId: Int!): CustomerAddressSummarywhoseaddressfield carries@sourceRow + @reference(path: [{key: "customer_address_id_fkey"}]); the canonical path-keyed shape. Backed byCustomerAddressSummaryJava record,CustomerAddressSummaryLifter.addressIdOf(parent) → Row1<Integer>, andCustomerAddressSummaryService. End-to-end coverage flows through the L5 compile-spec tier ongraphitron-sakila-example. Documentation. New how-todocs/manual/how-to/source-row.adoc(leaf-PK + path-keyed shapes with full SDL + Java + rejection-message anchors). Renamed referencedirectives/batchKeyLifter.adoc→directives/sourceRow.adocrewritten for the flat-args directive. Sweep acrossexternal-code.adoc(drops@sourceRowfrom theExternalCodeReferencetable; cross-link to the dedicated how-to),result-types.adoc,record.adoc,notGenerated.adoc,condition.adoc,handle-services.adoc, and the reference / how-to indexes. Internalrewrite-design-principles.adocupdated for the sub-seal and renamed classifier keys. Architect-review revisions (3b7f432) tightened theLifterKeyedJavadoc to describe today vs. tomorrow without overclaiming, dropped uninstall AsciiDoc tag markers fromschema.graphqls(the docs build is plain Asciidoctor with no example resolver, sotag::sourcerow-leafpk[]/tag::sourcerow-story-1[]had no consumer), and filedR116(composite-key-row2-source-row-coverage) for the composite-key Row2 path-keyed coverage gap (the resolver admits Row2..Row22 today; the gap is in the test catalog, no 2-column FK exists). Findings noted at approval (non-blocking, follow-ups not yet filed): (1) the LSP module’s hand-written directive registry was not updated for R110 ;graphitron-lsp/…/parsing/DirectiveDefinitions.java:77-80still definesbatchKeyLifterwith the obsoletelifter: ExternalCodeReference+targetColumns: [String!]!shape (nosourceRowentry),…/diagnostics/Diagnostics.java:45lists"batchKeyLifter"not"sourceRow"inVALIDATE_METHOD, and three test files (DirectiveDefinitionsTest,DiagnosticsTest,ClassNameCompletionsTest) pin@batchKeyLifteras part of the registry surface. The build is green because the LSP tests are self-consistent against their own hand-written registry, but the registry has drifted fromdirectives.graphqlsso an IDE consumer will surface "unknown directive" diagnostics on@sourceRowand continue to suggest a removed@batchKeyLifter. The flat-args shape may require extendingDirectiveDef/InputTypeBindingto express non-ExternalCodeReferencearg shapes, so this is a small standalone follow-up rather than a rework gate; the spec body framed migration as "internal-only" because adoption was minimal, but the LSP is a user-facing consumer of the directive surface that the spec didn’t enumerate. (2)docs/manual/reference/diagnostics-glossary.adoc:103(thelifter-methodentry) describes the wrong lifter shape: it says “methodName:” (the actual arg ismethod:) and “(Set<Key>) → Map<Key, Value>” (that’s the@servicerows-method shape;@sourceRow’s lifter is `(parent backing class) → RowN<…>per the resolver and the howto). Looks like the entry was copy-pasted fromattempt-service-method(line 94) without retargeting. Both findings are tractable as small standalone Backlog items; neither blocks the architectural surface (sealed sub-seal, three classifier-check keys, single-arm switch collapse) which is sound. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
Demote
@asConnection+ same-table@nodeIdguard from rejection to advisory warn (narrow-asconnection-same-table-nodeid-guard, R113,afedb4b8+846f055d+e5358818+7d59ffd5): R106 lifted same-table@nodeIdargs from aQueryLookupTableFieldlookup to aQueryTableField+BodyParam.Infilter on the connection rail; one inherited rejection from the lookup era did not compose. R113 first pass narrowed the rejection to the conjunctive∃ required same-table @nodeId leafpredicate (pathRequired = outer arg && every nested input wrapper non-null) and collapsed the carrier into a sealedAsConnectionGuard.{None | Required(SameTableHit)}. Production schema (opptak-subgraph’sQuery.kompetanseregelverkGittIdV2(ider: [ID!]! @nodeId(typeName: "Kompetanseregelverk")): [Kompetanseregelverk!] @asConnection) deliberately composes that shape to ship a paginatedWHERE pk IN (decoded_ids)connection to consumers; the rejection blocked a wire format the producer authored on purpose. Rework demoted the rejection arm to aLOG.warnatFieldBuilder.resolveTableFieldComponents; classification falls through toQueryTableField+FieldWrapper.Connectionand the connection emitter ships the expected SQL. With the build break gone the sealed two-arm carrier collapsed further to a single nullableSameTableHit firstRequiredSameTableHitfield onNodeIdArgPlan(architect-review tightening ; sealed sub-taxonomy was justified to gate a rejection, not a single warn site). The warn routes throughASCONNECTION_HYGIENE_LOG = LoggerFactory.getLogger(FieldBuilder.class.getName() + ".asConnectionSameTableHygiene"), mirroring theBuildContext.idRefShimprecedent: stable category address for log filters and migration tooling, independent ofFieldBuilderclass organisation.formatAsConnectionSameTableRejectionrenamed toformatAsConnectionSameTableWarningwith advisory rather than directive prose; still names field/leaf/typeName for migration tooling to grep on. Conjunctive ∃-required walk and cycle-protection scoping (add on entry, remove on return so sibling subtrees sharing an input-type subgraph each get visited independently) unchanged from the first pass. Tests: pipeline-tierNodeIdPipelineTest.NodeIdConnectionAdvisoryCase(8 cases, all_ALLOWED; required arg/input field/conjunctive cases assertQueryTableField+FieldWrapper.Connection+BodyParam.Inon PK + pagination components, structurally identical to the optional cases R106 already shipped; the carrier flip from rejected→allowed is visible as a rename + assertion-shape change). Unit-tierAsConnectionSameTableWarnFormatTest(onerequiredLeaf_emitsWarn_namingFieldLeafAndTypecase via logbackListAppenderon the category logger; pins field/leaf/typeName +every page of @asConnection would equal the input setheadline +make 'ids' nullableadvisory hint ; the stable bits migration tooling can grep on). Execution-tierGraphQLQueryTest.filmsConnectionByRequiredIds_idsSupplied_paginatesBoundedSetmirrors the production shape (required outer wrapper on a same-table@nodeIdlist arg composed with@asConnection): three ids supplied withfirst: 2returns 2 withhasNextPage=true; page 2 after the cursor returns the remaining 1 withhasNextPage=false. Out of scope: directive-based warn suppression (three silencing routes already exist ; nullable leaf, drop@asConnection, FK-target arg);@LoadBearingClassifierCheckannotation (architect-review verified hygiene-only ; annotating would be inert); FK-target@nodeId+@asConnection(composes today viaResolved.FkTarget.DirectFk→BodyParam.In/Eq/RowIn/RowEq); implicit scalar-ID-arg path (synthesised, not authored); element-level nullability inside an outer-required list (the list is bounded once the outer wrapper is non-null). Editorial follow-up noted at approval (not blocking): stale Javadoc atFieldBuilder.java:258referencesNodeIdArgPlan.AsConnectionGuard.Requiredand "rejection message" ; both removed by the carrier collapse. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
Multi-hop
@referencepath on@nodeIdfilter input fields, identity-carrying lift (multi-hop-nodeid-reference-filter, R114,b80594ff+0efba89f+0d12cc1):NodeIdLeafResolver.resolveFkJoinPathnow accepts@reference(path: […])of length ≥ 2 on@nodeId(typeName: T)filter input fields and arguments when every step is aJoinStep.FkJoinand every adjacent pair satisfies the lift predicate (each step’s source-side columns are a positional subset of the previous hop’s target-side columns by SQL name). The terminal hop’s source-side tuple lifts back through the chain to a sub-tuple of the first hop’s source-side columns, on the parent’s own table, positionally aligned with the decoded NodeType keys. TheDirectFkvsTranslatedFkdecision switches fromjoinPath.get(0)tojoinPath.getLast(); the resolver’sResolved.FkTarget.DirectFkgains aliftedSourceColumns: List<ColumnRef>slot and the four reference carriers (InputField.{Column,CompositeColumn}ReferenceField,ArgumentRef.ScalarArg.{Column,CompositeColumn}ReferenceArg) gain the matching slot, populated at carrier construction. Emitters atFieldBuilder.projectFiltersandFieldBuilder.walkInputFieldConditionsswapJoinStep.FkJoin) joinPath().get(0.sourceSideColumns()forliftedSourceColumns()read from the carrier; the emitted SQL is the same direct row predicate (field.eq/in(…)for arity 1,DSL.row(…).eq/.in(…)for arity ≥ 2) single-hop direct-FK already produces. Chain length is purely a classifier-time concept; the runtime touches one table, no JOIN, no subquery. Multi-hop is always explicit: the auto-discovery fallback (JooqCatalog.findUniqueFkToTable) stays single-hop only, so disambiguation amongA → ? → Cchains is the author’s responsibility via per-hop{ key: … }. Two distinct@LoadBearingClassifierCheckkeys (not one widened key):nodeid-fk.direct-fk-keys-matchwidens to "the terminal hop’s target-side columns positionally match NodeType key columns"; newnodeid-fk.identity-carrying-liftcovers "every intermediate hop satisfies the lift predicate so the lifted tuple is well-defined and lives on the parent’s own table". Independent invariants for independent future relaxations. Diagnostics are anchored onstatic final String LIFT_FAILURE_MARKER = "identity-carrying FKs"andCONDITION_STEP_MARKER = "must be a foreign key"constants onNodeIdLeafResolver; tests assert against the constants by name rather than copying prose. Wider migration of R57’s substring-based assertions to constant markers filed as a sibling. Tests: unit-tierNodeIdLeafResolverTest(3 new cases ;multiHopIdentityCarryingLift_succeedspins the lifted tuple shape on thelevel_a/b/cchain,multiHopLiftTranslationRejectedanchors onLIFT_FAILURE_MARKER,multiHopConditionStepRejectedanchors onCONDITION_STEP_MARKER); pipeline-tierNodeIdPipelineTest.{ArgumentFkTargetNodeIdCase.MULTI_HOP_IDENTITY_CARRYING, InputFieldFkTargetNodeIdCase.MULTI_HOP_IDENTITY_CARRYING_INPUT}pin the carrier-side identity (joinPath.size() == 2,BodyParam.RowIn.columns()SQL names =(k1, k2), decode method =decodeLevelA); pipeline-tierQueryConditionsPipelineTest.multiHopIdentityCarryingLift_emitsHelperOnLiftedTuplepins helper-method emission (body-string assertions banned per the test-tier rules; the L3 BodyParam-level case pins the SQL-shape lift). Compilation-tier coverage rides onmvn install -Plocal-db’s `graphitron-sakila-examplecompile (the lifted-tuple type aligns withdecode<TypeName>helper signatures). Newnodeidfixturechain:level_a(PK(k1, k2)),level_b(PK(s, k1, k2), FK tolevel_aon(k1, k2)),level_c(PK(c, s, k1, k2), FK tolevel_bon(s, k1, k2)), pluslift_fail_{a,b,c}for the translation-failure case; both metadata-registered inNodeIdFixtureGenerator. Howto article atdocs/manual/how-to/multi-hop-nodeid-filter.adoc(mental-model first, worked example, two rejection-message sections anchored on the marker constants); SDL is inlined inside the article because thenodeidfixturejOOQ classes live in a separate package from the sakila-example’sjooqPackage(tag::switch lands with the L6 wiring follow-on). Honest deviations carried forward: the L6 execution-tier round-trip (GraphQLQueryTest.multiHopReferenceFilter_returnsRows, asserting "single-table FROM, no subquery" viaExecuteListener) is deferred to a Backlog sibling because wiringnodeidfixture.level_*into the example needs either a second graphitron-codegen execution or duplicated tables under the sakila-example’s public schema, neither of which is in scope for the carrier-shape change R114 owns. The same precedent already applied to R50’sparent_node/child_reffixture. Out of scope, follow-ups filed: non-identity-carrying multi-hop@referenceon@nodeId(EXISTS-subquery / JOIN-with-translation emission, symmetric to R57’s single-hop translated FK case); renamecolumn/columnsslot on the four reference carriers to a role-explicit name (the slot holds NodeType key columns on the target table but reads as "the predicate column"); diagnostic-anchoring policy migration of R57’s substring-based assertions; L6 execution-tier round-trip;Resolved.FkTarget.DirectFk.fkSourceColumnsvestigial slot (now fully covered byliftedSourceColumns). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25;LoadBearingGuaranteeAuditTestpicks up the new key’s producer/consumer pairs automatically. -
Make graphitron-maven-plugin IT self-contained via extraArtifacts (
maven-invoker-it-extra-artifacts, R111,11f276184): the two ITs undergraphitron-maven-plugin/src/it/(basic-generate,missing-schema-inputs) failed inRewrite reactor CIbecause the forked child Maven could not resolveno.sikt:graphitron-sakila-db:10-SNAPSHOT; CI runsverify, which never installs sibling reactor modules into~/.m2, andinvoker:installonly seeds the IT local-repo with the project under test plus its declared dependency tree. Add<extraArtifacts><extraArtifact>no.sikt:graphitron-sakila-db:${project.version}</extraArtifact></extraArtifacts>to themaven-invoker-pluginconfiguration ingraphitron-maven-plugin/pom.xmlso the IT’s missing sibling rides the same reactor-cache resolutioninvoker:installalready uses, and rewrite the contract comment insrc/it/settings.xmlto name<extraArtifacts>as the seam for sibling-module IT deps so the next contributor adds an entry there rather than reintroducing an implicitmvn installprerequisite. Tests: no new IT ; adding a third invoker IT to lock the seam down would mean inventing a synthetic sibling-module dependency; the rewritten settings.xml comment serves the doc-of-record role, and theRewrite reactor CIworkflow is the regression rail. Verification: with the cached snapshot wiped (rm -rf ~/.m2/repository/no/sikt/graphitron-sakila-db),mvn -f graphitron-rewrite/pom.xml verify -Plocal-db --batch-moderuns both ITs to SUCCESS where they previously failed withCould not find artifact …graphitron-sakila-db:jar:10-SNAPSHOT. Out of scope: switching CI fromverifytoinstall(would mask the issue and pollute the runner cache); profile-gating the entry (unconditional and harmless either way); auditing other reactor modules for similar issues (graphitron-maven-pluginis the only IT-housing module today). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
@record-parent multi-table polymorphic ChildField classifier arm (record-parent-multitable-polymorphic-classifier-arm, R105,76c3262f+518ffc70+c7c3579c+21af55604): theReturnTypeRef.PolymorphicReturnTypearm ofFieldBuilder.classifyChildFieldOnResultTypeis no longer a blanketRejection.deferred("@record type returning a polymorphic type is not yet supported", ""). Three of the fourBatchKey.RecordParentBatchKeypermits become reachable on@record-backed parents:RowKeyedwhen the parent is aJooqTableRecordType(hub = parent’s mapped table);AccessorKeyedManywhen the parent is aPojoResultType/JavaRecordTypeexposing a unique zero-argList<X> / Set<X>-returning accessor for some concreteX extends TableRecord(hub = accessor’s element-Record table).AccessorKeyedSingleis structurally derivable but deferred at the classifier (Rejection.deferred("polymorphic-child-record-parent-single-cardinality")):MultiTablePolymorphicEmitter.buildScalarPerParentFetcherreads parent context asRecord parentRecord = (Record) env.getSource()and has no@record-Pojo arm, so producing the permit there would generate code that ClassCastExceptions at runtime on a Pojo source.LifterRowKeyedfor polymorphic returns stays deferred per Out of scope (@batchKeyLifter’s `targetTablederivation reads the field’s@tableelement type, which doesn’t apply to polymorphic returns). The hubTableRefis consumed at classification time (handed toresolveChildPolymorphicJoinPathsfor per-participant FK auto-discovery) and never re-read after the field record is constructed, so it stays a classifier-internal local rather than a slot on the field record. New builder-internal sealed resultPolymorphicRecordParentResolution.{Resolved(parentKey, hubTable) | Rejected(rejection)}per the principles' "Builder-step results are sealed" rule. New shared private helpercollectAccessorMatchesfactored out ofderiveBatchKeyFromTypedAccessor;deriveBatchKeyFromHubAccessoris the polymorphic-callsite sibling whose reduction step discovers the hub from the unique resolvable accessor rather than pinning against an external@table(none on a polymorphic return). Themultitable-polymorphic-child.parent-key-extraction-is-batchkey-driven@LoadBearingClassifierChecksplits per-producer (…-table-backedonclassifyObjectReturnChildField,…-record-parentonclassifyChildFieldOnResultType) per the audit’s one-producer-per-key rule; the twoMultiTablePolymorphicEmitterconsumer call sites (buildBatchedConnectionFetcher,buildBatchedListFetcher) gain a second@DependsOnClassifierCheckfor the new key via the repeatable annotation. Newaccessor-rowkey-shape-resolved-against-hubkey onderiveBatchKeyFromHubAccessor: same-shape sibling of the existingaccessor-rowkey-shape-resolvedbut the identity contract is hub discovery rather than expected-table match.validateChildMultiTableParentPk(GraphitronSchemaValidator.java:347) drops theTableBackedType-gated early-return and readsfield.parentKey().preludeKeyColumns()uniformly across all fourRecordParentBatchKeypermits; signature losesparentTypeName, Map<String, GraphitronType> typesand gains the field reference, with both call sites updated. The non-empty invariant moves entirely upstream:RowKeyed’s canonical constructor and `JoinStep.LiftedHop’s constructor both reject empty key columns at construction time, and the classifier routes empty-PK / unresolved-hub parents through `UnclassifiedField. Architect-review type-system tightening:ChildField.{InterfaceField, UnionField}canonical constructors enforce non-nullparentKeyandparentResultTypeviaObjects.requireNonNull, lifting the validator’s de-facto contract into the type system. Tests: pipeline-tierRecordParentMultiTablePolymorphicPipelineTest(new file undergraphitron/src/test/java/no/sikt/graphitron/rewrite/) drives the SDL → classifier path for all reachable permits ;childInterfaceField_recordParent_rowKeyedand_typeSpecEqualsTableBacked(parity-pin between the @record-JooqTableRecord producer and the table-backed producer viamethodSpec.toString()comparison so any drift across the two construction sites fails fast),_accessorKeyedMany(hub identity offLiftedHop.targetTable(),LOAD_MANYdispatch),_accessorKeyedSingle_deferred(DEFERRED rejection assertion for the Pojo + single-cardinality shape), andrecordParentPolymorphic_pojoWithoutMatchingAccessor_classifiesAsUnclassifiedField(three-option AUTHOR_ERROR with hub-author-error tail). UnionField siblings mirror the InterfaceField cases. Validator-tierInterfaceFieldValidationTestextends withrejects_listArm_onAccessorKeyedManyHubArityOver21(22-column hub PK onAccessorKeyedManytrips the same Row22 cap as the table-backedRowKeyedpath); the now-unreachablerejects_connection_onPkLessParentandrejects_listArm_onPkLessParenttests are dropped (empty-PK is unreachable through the canonical constructors). NewresultTypeFor(table)test-fixture helper publishes a sentinelJooqTableRecordTypefor the type-system non-null contract.GraphitronSchemaBuilderTest.NON_ERROR_POLYMORPHIC_FALLS_THROUGH_TO_DEFERRED_REJECTIONupdated: the Pet union fixture is Pojo-parent + single-cardinality, exactly the new deferred shape, and now lands onDEFERREDrather than the priorAUTHOR_ERROR. Out of scope, follow-ups: wideningMultiTablePolymorphicEmitter.buildScalarPerParentFetcherto consumeparentKey+parentResultTypeanalogously to the list arm (lifts the AccessorKeyedSingle defer); contract-on-the-field-record audit shape so consumers cite one key rather than the producer-key disjunction; per-participant constraint coverage beyondresolveChildPolymorphicJoinPaths; user-facing@record-with-polymorphic-children documentation. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
Lift same-table
@nodeIdarg/field to aWHERE pk IN (…)filter, not a lookup (nodeid-lookup-ignores-filter-siblings, R106,91c3cb892): same-typename@nodeIdargs on a table-bound query field now classify asQueryTableFieldwith aBodyParam.In/BodyParam.RowInpredicate against the table’s primary key, instead of the implicit promotion toQueryLookupTableField. Sibling filter args (scalar@condition/@field) compose with the@nodeIdarg as ordinaryBodyParampredicates rather than being silently dropped under the legacy lookup-promotion gate. Three classifier-seam edits inFieldBuilder.java: theResolved.SameTablearm readsarg.hasAppliedDirective(DIR_LOOKUP_KEY)instead of hard-codingisLookupKey = true(filter is the new default; explicit@lookupKeyre-enables the N×M derived-table shape); the blanket@nodeId @lookupKeyrejection is gone on the same-table arm and remains on the FK-target arm with a pointed message ("@lookupKey is meaningless on an FK-target @nodeId arg");classifyQueryField’s lookup-promotion gate drops the `lookupPlan.anyArgSameTable()half and is now purelyhasLookupKeyAnywhere(fieldDef). Pulls the same-table@nodeIdpath onto the same filter rail as FK-target@nodeId(Resolved.FkTarget.DirectFkalready lifts toBodyParam.In/Eq/RowIn/RowEq), collapsing two near-identical paths into one and making mixed-shape inputs first-class. Tests: pipeline-tierNodeIdPipelineTest.ArgumentSameTableNodeIdCasemigrates 4 cases fromQueryLookupTableField/ScalarLookupArg/DecodedRecordtoQueryTableField/BodyParam.In/BodyParam.RowInassertions; newSAME_TABLE_WITH_FILTER_SIBLINGcase pins the headline composed-with-sibling lift on a composite-PK NodeType; newSAME_TABLE_WITH_EXPLICIT_LOOKUP_KEYcase pins that explicit@lookupKeyre-enables the lookup shape; newFK_TARGET_LOOKUP_KEY_REJECTEDcase pins the new FK-target rejection. Execution-tierGraphQLQueryTest.filmsByNodeIdArgWithTitleFilter_composesPkInWithSiblingFilterexercises the lift end-to-end (PK-IN composed withWHERE title = ?); the existingfilms_filteredByArgNodeId_returnsRowsMatchingDecodedIdstest stayed green with its comment refreshed for the new shape. No@LoadBearingClassifierCheckkeys touched (audit-inert flip). Out of scope, follow-ups: collapsingNodeIdArgPlan.{anyArgSameTable, anyNestedSameTable, sameTableHit}into a sealedAsConnectionGuard.{None | Hit}carrier (the@asConnectionrejection atFieldBuilder.java:403-407is the only remaining consumer; clean follow-up); the implicit@lookupKeydirective walkerinputTypeHasLookupKey; FK-target@nodeId(already a filter, no behavior change). Editorial follow-ups noted at approval (not blocking): stale comments atFieldBuilder.java:1105("the same-table arm synthesises isLookupKey: true") andBuildContext.java:181("same-table lookup vs FK-target filter") describe pre-R106 behavior and want a refresh. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
RC parity audit: classify GraphitronField/Type leaves and ship coverage gaps (
rc-parity-audit-leaf-coverage, R104, Phase 1a/1b14386cfc8, Phase 1a tests8ddfb272f, Phase 1c5bab9ca7f, Phase 1da032c96f3, Phase 2e0ff1f923, Phase 3c1fcbdcb2, Phase 4ba8149a7f, self-review fixesb947098e8, README linkacbcd4799, docs-site stagingadc63948a, rework32e10769): funnel every classifier write through named operations that emit a JSONL trace, then ship a DuckDB-backed post-processor that joins the per-module traces with the sealed-permits inventory and roadmap mentions to produce a regenerable per-leaf coverage report and a consumer-facing migration-fragment.BuildContext’s previously bare `typesmap andGraphitronSchemaBuilder.buildSchema’s bare `fieldsmap becomeTypeRegistry/FieldRegistryprivate fields; type and output-field writes route throughclassify/enrich/demote/synthesize(each carrying a clean prior-entry precondition); the input-field path routes throughFieldRegistry.classifyInput(trace-only ; input fields are embedded in their parent type, not a central map, and the asymmetry is documented honestly rather than fought).ClassificationTraceis gated on-Dgraphitron.classification.trace=<path>; the new parent-pom-Pleaf-coverageprofile sets the property to${project.build.directory}/leaf-coverage.jsonlper module, with amaven-antruntruncate before the test phase so re-runs don’t append on top of stale records. A JUnit 5 extension auto-registered viaMETA-INF/servicesplusjunit.jupiter.extensions.autodetection.enabled=truetags every record produced inside a test’s lifecycle with the running test class and its tier annotation (resolved through the meta-@Tagon@UnitTier/@PipelineTier/@CompilationTier/@ExecutionTier, with@Tag("cross-cutting")exempted into a separate report column).roadmap-tool leaf-coverageopens an in-memory DuckDB connection, exposes the per-module JSONL files as a view viaread_json_auto('graphitron-rewrite//target/leaf-coverage.jsonl', union_by_name=true), stages parsedleaves(sealed permits + javadoc intent) andmentions(roadmap simple-name grep) tables, and renders both the internal report atgraphitron-rewrite/roadmap/inference-axis-coverage.adocand a consumer-facing--mode=migrationAsciiDoc fragment.directive-supportgains a sibling--mode=migrationrender. The migration guide atdocs/manual/how-to/migrating-from-legacy.adocinclude::`s both fragments under "Authoritative supported surface". Verify-mode of `roadmap-toolis bound to theverifyphase and fails CI when the README or the leaf-coverage report drift; the CI workflow now runsmvn verify -Plocal-db -Pleaf-coverageso the trace files exist for the verify check. *Tests: unit-tierTypeRegistryTest(8) andFieldRegistryTest(5) pin precondition contracts;ClassificationTraceTest(5) documents the JSONL framing including ThreadLocal context inheritance and JSON escape;ClassificationTraceContextExtensionauto-registers and tags every existing test’s classification records with its tier;LeafCoverageReportTest(5) covers parser hierarchy isolation, intent attribution, the nested-record-vs-sealed-parent worked case (MutationField/DmlTableField), and the roadmap-mention join;DirectiveSupportReportTestextension covers the--mode=migrationrender. Honest deviations carried forward: thedirective-supportmigration fragment has no verify-mode CI binding in roadmap-tool’s verify phase because that fragment readsgraphitron-common/src/main/resources/directives.graphqls(a legacy module the rewrite reactor explicitly does not resolve); it regenerates from the docs build instead. Follow-up filed: R107 capturesLeafCoverageReport.parseMentions’s simple-name join sensitivity (any roadmap edit that names a leaf class drifts the report). Out of scope, owned by the triage follow-up*: classifying each leaf as Covered / Trivial gap / RC-blocker / Defer and spawning sibling Backlog items per RC-blocker; this item ships the regenerable data the triage will read from. Build green: full `mvn -f graphitron-rewrite/pom.xml install -Plocal-db -Pleaf-coverageon Java 25. -
Extract
ConnectionPromoterfromGraphitronSchemaBuilder(extract-connection-promoter, R56,3f1c9af9+e94a1a9a): the ~250-line Connection-promotion concern (turning@asConnectioncarrier fields into proper Connection-typed fields, plus synthesising Connection / Edge / PageInfo entries onctx.typeRegistry) lifts into afinalpackage-private sibling undergraphitron/src/main/java/…/rewrite/.GraphitronSchemaBuilder.javashrinks 670 → 288 lines (well past the spec’s ≤440-line target); the orchestrator retains the two-call sequence (ConnectionPromoter.promote(ctx)→ConnectionPromoter.rebuildAssembledForConnections(…)) but the implementation moves. Pure structural extract-class ; no behaviour change, no sealedResolved(the spec carved out why: this is a single-step structural transformation with no rejection arms; rejection of malformed@asConnectionusage already lives upstream inFieldBuilder.classifyField). The localbaseTypeName(GraphQLOutputType)helper migrates as a private static on the new class rather than being reconciled againstBuildContext.baseTypeName(GraphQLFieldDefinition)(different signatures, different unwrap semantics; consolidation is a separate decision if it ever matters). Tests: newConnectionPromoterTest(@UnitTier) exercises promotion directly via the existingGraphitronSchemaBuilder.buildContextForTestsseam (the R40 test-only entry point that runs the schema generator +TypeBuilderbut stops before field classification) ; eight focused-unit cases (directive-driven bare-list carrier, explicitconnectionName:, explicitdefaultFirstValue:, structural Connection-typed return enrich-path, SDL-declared@shareablePageInfoflag preservation, two-carrier dedup, return-type already names the Connection emits-no-rewrite, item-nullability propagation) plus anoSynthesisedTypesshort-circuit regression onrebuildAssembledForConnections. Existing pipeline-tier coverage (GraphitronSchemaBuilderTest@asConnectioncases,ConnectionRegistrationsTest,ConnectionTypeValidationTest) stays green as regression. TheDIR_AS_CONNECTIONdirective-presence assertion invalidateDirectiveSchemastays put (out of scope by spec). Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbon Java 25. -
Batched key extraction for
ChildField.UnionField/ChildField.InterfaceFieldviaBatchKey(batch-multitable-polymorphic-child-fetcher, R102, Phases A-Df50f80b1, Phase Ed42eeebf, scalar-fetcher cleanupe1e45122, rework13eefedb, javadoc cleanup at approval): the multi-table polymorphic emitter no longer bypassesGeneratorUtils.buildRecordParentKeyExtraction; both arms (list and connection) readparentKey: BatchKey.RecordParentBatchKeyandparentResultType: GraphitronType.ResultTypeoff the field record and delegate to the canonical four-shape × four-permit key-extraction helper. The list arm gains DataLoader-batched fetch (one stage-1 UNION ALL withJOIN parentInputplus one per-typename SELECT, regardless of parent count);AddressOccupantsListBatchingTestpins the canonical sakila customer fanout to exactly 4 statements (down from ~14 pre-R102).BatchKeypermits with key-column components now enforce non-emptiness via compact canonical constructors (six direct + three viaJoinStep.LiftedHop’s slot-list invariant); `containerType’s dead empty-list fallbacks at `BatchKey.java:124, 133removed.validateChildConnectionParentPkrenamed tovalidateChildMultiTableParentPk, drops the Connection short-circuit, fires uniform N=21 cap on both arms (the sharedparentInput VALUESemitter widens toRow<N+1>, topping out at jOOQ’sRow22); the codegen-timeIllegalStateExceptionatMultiTablePolymorphicEmitter.java:682-693is removed in favor of the validator rejection. Newmultitable-polymorphic-child.parent-key-extraction-is-batchkey-driven@LoadBearingClassifierCheck↔@DependsOnClassifierCheckpair onFieldBuilder.classifyObjectReturnChildField(producer) and the two batched fetcher entry points (consumer).parentTableparameter dropped from everyMultiTablePolymorphicEmitterhelper signature; the dispatch site atTypeFetcherGenerator.java:436-461collapses accordingly. Tests: audit-tierBatchKeyTest(parameterised non-empty case per permit +JoinStep.LiftedHop); pipeline-tierTypeFetcherGeneratorTest(list-form DataLoader registration, key-tuple arity for single-PK and composite-PK parents, Interface/Union equivalence pin,buildRecordParentKeyExtractiondelegation pin);InterfaceFieldValidationTestandUnionFieldValidationTestmirror empty-PK and>21arity rejections plus 21-boundary well-formed cases on both list and connection arms; execution-tierAddressOccupantsListBatchingTestagainstAddress.occupants: [AddressOccupant!]!. Spec deviations carried forward: lifting the connection-rows participant single-PK truncation atMultiTablePolymorphicEmitter.java:824into the validator deferred (existingQuery.pagedItems → PagedA/PagedBwith composite(k1, k2)PK quietly works on the truncation; promoting it to a hard error would block landing). Thewrapperparameter for gating that check on Connection re-lands when the lift does. Out of scope, owned by R105:@record-parent classifier arm; lighting up theLifterRowKeyed/AccessorKeyedSingle/AccessorKeyedManypermits at classification time. R102 shipsRowKeyed-only; the slot type accepts the full four-permitRecordParentBatchKeysub-seal so R105 wires that arm in without re-touching the records or the emitter. Build green: graphitron module 1434/1434 on Java 25. -
@recordaccessor resolution validated at classify time (record-accessor-validation, R88,0bcb6ebe+b2d798a9+e5f2dd2e+863d90c5): downstream consumers gotcannot find symbol: getSakId()fromjavacon generated fetcher classes when the SDL field name didn’t match an accessor on the@record-backed POJO/Java-record (FetcherEmitter.propertyOrRecordValuesynthesised the getter name purely from"get" + capitalize(toCamelCase(columnName))without consulting the backing class). The fix lifts accessor resolution into the classifier per Classifier guarantees shape emitter assumptions and validator mirrors classifier invariants: newClassAccessorResolver.resolveruns reflective lookup atClass.forNameboundary (added to the reflection-roster atdocs/rewrite-design-principles.adoc:29), returning sealedAccessorResolution.{Resolved | Rejected}withResolveddirectly sealed overGetterPrefixed(Method) | BareName(Method) | FieldRead(Field). Resolution rules track graphql-java’sPropertyDataFetcherlookup order (get<CamelName>→is<CamelName>for boolean → bare<camelName>→ public field), enforce return-type assignability against the SDL field’s resolved Java type, and match either a singleDataFetchingEnvironmentparameter or per-arg parameters whose types match the SDL arg list. Phase E’s type-system tightening narrows the slot onPropertyField.accessor/RecordField.accessorfromAccessorResolutiontoAccessorResolution.Resolved(still nullable for parents that don’t run reflective resolution: jOOQ-record-backed and null-fqClassNamePropertyDataFetcher-fallback parents);FieldBuilderroutesRejectedthroughUnclassifiedFieldcarrying a newRejection.AuthorError.AccessorMismatcharm whosemessage()appends the@field(name: "…")override hint inside the typed arm so discrimination rides on the seal rather than a message prefix;FetcherEmitterswitches exhaustively over the threeResolvedarms with no runtime fallback. Theclass-accessor-resolver-shape-guarantee@LoadBearingClassifierCheck↔@DependsOnClassifierCheckpair documents the contract; the consumer’sreliesOntext now describes a static type guarantee rather than a runtime invariant. Tests: pipeline-tierRecordFieldAccessorValidationTest(10 cases ; three rejection arms each pinning theUnclassifiedFieldrouting structurally + producing actionable validator diagnostics; six positive arms exercisingGetterPrefixed,BareName,FieldRead, full-env injection, per-arg injection, override-via-@field(name:)); execution-tierRecordExampleTypefixture (all three fields resolve toResolved, exercising the emit / execute boundary); audit-tierLoadBearingGuaranteeAuditTestpicks up the producer/consumer pair automatically. Implementation deviation: per-arg injection at the emitter usesMethod.getParameters()for argument names, requiring the backing class to be compiled with-parameters;methodCallExprthrows at emit time with a clear error if absent rather than producing silently-broken code. Out of scope, filed as follow-ups: input-side ctor/setter validation for@record-mapped input types;PropertyDataFetcherfallback removal at the null-fqClassNamearm; Lombok / explicit-named accessor extensions; Levenshtein "did you mean" candidates. Build green: fullmvn -f graphitron-rewrite/pom.xml install -Plocal-dbacross all 11 reactor modules on Java 25 (modulo two pre-existing unrelated trunk failures last touched by R79/R82). -
LSP quick-fix and directive-vocabulary registry for the
ExternalCodeReference name → classNamemigration (lsp-externalcodereference-name-migration, R93, Phase 119e18b23, Phase 25258d16f, Cycle 275bee87b): two consumer-facing landings on the LSP. Phase 1 introducedDirectiveDefinitionsas the LSP’s directive-vocabulary registry (keyed on directive name; each entry carries(argName, inputType, nestedPath)tuples) and migratedClassNameCompletions.outerArgOf’s hardcoded three-directive lookup onto the derived view `argsByInputType("ExternalCodeReference"). The five sites that appeared indirectives.graphqlsbut were unwired in the LSP today (@externalField,@enum,@tableMethod,@batchKeyLifter, and the nestedReferenceElement.conditioninside@reference(path:)) gained completion + diagnostic surface as a side effect. Phase 2 added the code-action surface:SdlActionprimitive (namedDetector/Rewriteinterfaces, sealedRewriteResult.{Edit | Skip}so the bulk action’s count-by-reason pivot is typed) wrapping a single instantiation for thename → classNamemigration withtargets = { Member("ExternalCodeReference", "name") }; three activation points (per-site quick-fix on the cursor, file-scoped bulk action, workspace-scoped bulk action emitting a multi-documentWorkspaceEditdirectly with noexecuteCommandindirection); diagnostic stance splits on resolution: legacy-and-resolves stays silent (the build-channelLOG.warninFieldBuilder.parseExternalRefis the migration-tracking signal), legacy-and-unresolved fires error-severity diagnostic mirroring the build’sExternalRef.lookupErrorarm and naming the unresolved name plus the two fixes (namedReferencesconfig or writeclassName:directly). Bidirectional drift protection between SDL and theSdlActionsregistry: everySdlAction.targets()entry must resolve againstdirectives.graphqls’s deprecation markers (SDL `@deprecated()for member-level; structured javadoc-style@deprecated <reason>token in directive description strings for whole-directive); every marker must be covered by either anSdlActionor theMANUAL_MIGRATION_DEPRECATIONSallow-list (at landing:Member("@asConnection", "connectionName")per-instance semantics,WholeDirective("index")deferred to a future per-call-site rewrite).@index’s description string converts the legacy prose "Deprecated: use `@order(index:)instead" to the structured token form soDeprecationMarkerscan pick it up; no semantic change for consumers.CompletionDatagained anamedReferencesslot (4-arg canonical constructor; 3-arg secondary kept for test fixtures);Workspace.openUris()exposed for the workspace-scoped bulk action. Tests: unit-tierDirectiveDefinitionsTest,SdlActionTest(7 cases),DeprecationMarkersTest(10 cases including bundleddirectives.graphqlsparse),SdlActionDriftTest(4 cases including the at-landing-time canonical-set pin); LSP-tierCodeActionsTest(8 cases, including the cycle-2 sibling-diagnostic regression seam asserting the per-site quick-fix surfaces independently ofCodeActionContext.diagnostics),DiagnosticsTestextension (9 cases: one resolves-silent fixture, eight per-site unresolved-error fixtures one per ECR-binding directive, plus a canonical@servicemessage-content assertion naming the unresolved name and pointing at the two fixes),ClassNameCompletionsTestextension (existing three-site cases pass after the migration; five new cases cover the previously unwired sites). 171 LSP tests, 0 failures; full graphitron-rewrite install green on Java 25. Carried follow-ups (not in scope, captured for nextSdlActionauthor):CodeActions.countableNounignores its parameter and hardcodes the R93 noun (fine for one action; brittle once a secondSdlActionlands);applyAll/countResolvable/countSkippedeach iterate matches and re-invoke the rewrite, three full passes per file per request (correctness-clean, just wasteful ; a single partition pass producing(edits, skipCount)together would be cleaner). Out of scope: concrete-FQN suggestions for unresolvedname:values (deferred until R90 Phase 3’s static-method index lands); renaming the@externalFielddirective itself (R54, disjoint SDL surface); automating consumernamedReferencesconfig edits. -
Breaking: missing-vs-null semantics on single-row DML mutations now PATCH-shaped (
bulk-dml-mutations, R77 Phase B/C,4cb22014e+38e1d5fa3): single-row INSERT/UPDATE/UPSERT no longer write SQL NULL on every classifier-known column regardless of whether the input map carried the key. Insert-side cells now bindDSL.defaultValue(dataType)when the input omits the key (the column default lands; onNOT NULLcolumns without a default, this surfaces as a NOT-NULL violation rather than the silent null write);DSL.val(value, dataType)when the input carries the key (explicitnullwrites SQL NULL via typed null binding). Update-side SET clauses are now built from a runtimeif (in.containsKey(name)) { sets.put(…) }walk overtia.setFields(): omitted columns drop out ofSETentirely, preserving the existing row’s value (PATCH semantics); explicit-null columns write SQL NULL. The UPSERT update branch shares the same dynamic SET walk overDSL.excluded(col), so an omitted column is no longer overwritten byEXCLUDED.colon conflict (which, paired with the insert-sideDEFAULTcell, was silent data loss whenever the column had a default). Migration: callers that relied on the old "always write SQL NULL" behavior for omitted columns must set the field to explicitnullin the input map; graphql-java’s argument coercion preserves the absent-vs-null distinction in the resultingMap<String, Object>(Map.containsKey is the dispatch key). The structural pin lives inFetcherPipelineTest.dmlInsertField_*containsKey*/dmlSingleRowUpdateField_emitsDynamicSetWalkOverInKeySet; execution-tier coverage inGraphQLQueryTest.createFilm_omittedFieldUsesColumnDefault,createFilm_explicitNullRaisesError,updateFilm_omittedFieldLeavesColumnAlone_explicitNullWritesNull,upsertFilm_omittedFieldOnInsertBranchUsesColumnDefault,upsertFilm_omittedFieldOnUpdateBranchLeavesColumnAlone. Phase E (44f3a6e0e+afd520e47) extended the same dispatch to the bulk arms across all four verbs and added uniform-shape, no-set-fields-present, and duplicate-lookup-key guards on bulk UPDATE plus the per-armvalueTypelift, the centralized empty-list short-circuit, and the inline Postgres-only dialect guard on bulk UPDATE; Phase F (87cfa4814+9dd81d093) shipped Sakila execution-tier coverage for the four bulk verbs (DmlBulkMutationsExecutionTest, 18 tests covering bulk INSERT/UPDATE/UPSERT/DELETE projection, missing-vs-null pairs on INSERT/UPDATE/UPSERT, divergent-shape and only-lookup-key rejection paths on bulk UPDATE/UPSERT, duplicate-lookup-key guard on bulk UPDATE, empty-list short-circuit per verb) plus the two single-row only-lookup-key analogues (updateFilm_onlyLookupKeyFields_raisesError,upsertFilm_onlyLookupKeyFields_raisesErrorinGraphQLQueryTest); Phase G (37622aa75) scrubbed eleven stalemutations.mdcitations acrossDmlReturnExpression,FieldBuilder,TypeFetcherGenerator,GraphitronSchemaBuilderTest, andTypeFetcherGeneratorTest. Spec deviation acknowledged at approval: theupsertFilms_doNothingMode_skipsUniformityGuardexecution test routed to the pipeline tier (FetcherPipelineTest.dmlUpsertField_bulkInput_doNothingMode_omitsUniformShapeAndSetMapEmits) because PostgreSQL enforces NOT-NULL before evaluatingON CONFLICT, so the doNothing-with-divergent-shapes SQL can’t be exercised against Sakila’sfilmtable; the pipeline-tier substitute pins the structural claim (nofirstKeyscapture, nosetsUpdatewalk, no.doUpdate()clause,.onConflict(…).doNothing()chain present, bulkList<Map<?,?>>cast preserved). Closing landing: In Progress → In Review ate9746c7ea; In Review → Done approved on this commit. -
Path expressions in
argMapping(argmapping-path-expressions, R84,b3f85cd–91dd082): the right-hand side of anargMappingentry on@service/@tableMethod/@conditionmay now be a dot-path that walks into nested input fields (e.g.kvotesporsmal: input.kvotesporsmalId). New sealedPathExpr.{Head | Step}carrier replacesMap<String, String>onArgBindingMap.byJavaName;ArgBindingMap.of(slotTypes, segmentChains)walks segment chains against the GraphQL schema and populates a per-stepliftsListflag so the emitter never re-asks the schema. NewResult.PathRejectedarm covers structural rejections (walk-through scalar/enum/union/interface; unknown segment with closest-match hint). Multi-segment flat paths route through the existingCallSiteExtraction.NestedInputFieldmachinery; intermediate-list paths route through a newArgCallEmitter.buildListAwarePathExtractionwalker that emits element-wise.stream().map(…).toList()for eachliftsList=truesegment (one-list-deep and two-list-deep shapes both supported).selection.parseEntries(raw)extracts pure syntax (tokens → segment chains) so R69 can consume it directly.ServiceCatalog.reflectServiceMethodparameter-mismatch hint mentions path expressions on every rejection that prints anargMappingexample, and pre-fills a concrete reachable path when exactly one matches the unmatched parameter’s Java type across the field’s slots (7-arg overload threads slot types fromServiceDirectiveResolver). Spec deviations (acknowledged at approval time):Result.PathRejectedreplaces the spec’spathErrorcarrier slot since path resolution lives inArgBindingMap.ofand a separate slot would be redundant metadata; the parallel walker inArgCallEmitterwas preferred over augmentingNestedInputFieldwith per-segmentliftsListto avoid threading an always-falseflag through every R63 site. Deferred: enum/text-map/NodeId leaves combined with intermediate-list paths (no live consumer; emitter rejects the combination with an actionable message). Tests:ArgBindingMapTest(22 cases ; head/step shapes, list-shaped intermediates, scalar walk-through rejection, unknown-segment with candidate hint);ServiceCatalogTestPhase F cases (floor mention, no-args negative, stretch unambiguous-prefilled, ambiguous-fallback, type-mismatch-fallback, 6-arg-overload-fallback); execution-tierGraphQLQueryTestsakila fixturesfilmsByPath(one-step),filmsByListPath(one intermediate list),filmsByNestedListPath(two intermediate lists). Build green across all four tiers on Java 25. -
Sealed resolution outcomes for catalog table/FK lookups (
catalog-resolution-sealed-outcomes, R81,c48e532):JooqCatalog.findTable(String)now returnsTableResolution.{Resolved | NotInCatalog | Ambiguous(schemas)}andfindForeignKeyByName(String)returnsForeignKeyResolution.{Resolved | NotInCatalog};BuildContext.synthesizeFkJoinreturnsFkJoinResolution.{Resolved | UnknownTable(name, failure) | UnknownForeignKey(fkName)}so the fourOptional<TableRef>and fourOptional<FkJoin>rejection sites switch on variant directly instead of fabricating eight distinct "not in catalog" strings.JoinStep.FkJoin.fkis non-null by canonical-constructor enforcement; the redundantString fkNamecomponent drops (carriers readfk.sqlName()). Catalog construction asserts every schema in the live jOOQ catalog publishes a generatedTablesclass, throwingIllegalStateExceptionpointing at<tables>true</tables>on miss;TableEntry.toTableRefis consequently non-Optional.findCandidateSchemasFordeletes (theAmbiguousarm carries the schema list inline);findUnqualifiedTabledrops.limit(2)sinceAmbiguousneeds the full list. Diagnostic-builder consolidation:BuildContext.unknownTableRejectionswitches onTableResolution(Ambiguous → structural with qualified-form suggestions; NotInCatalog → unknownTable with Levenshtein candidates); new siblingunknownForeignKeyRejectioncovers FK-name misses.ServiceDirectiveResolver.computeExpectedServiceReturnTypejavadoc drive-by:<jooqPackage>→<schemaPackage>(post-R78 phrasing). 26new FkJoin(name, null, …)test fixtures migrate toTestFixtures.foreignKeyRef(…)factory. Tests:JooqCatalogMultiSchemaTestextends to 45 cases ; threeTableResolutionarms by name on the multischema fixture (widgetresolved,eventambiguous in both schemas, fabricated namesNotInCatalog); bothForeignKeyResolutionarms; all threeFkJoinResolutionarms viasynthesizeFkJoin; the construction precondition (staticverifyTablesClassPresenthelper); both diagnostic builders assertingRejection.AuthorError.UnknownNameshape andAttemptKind.FOREIGN_KEYtagging. TheFkJoinResolution.UnknownForeignKeyarm is structurally unreachable from current production callers (they pre-resolve the FK viafindForeignKeybefore callingsynthesizeFkJoin); the taxonomy still expresses completeness so future call sites must handle the shape, and a unit test asserts the variant constructs correctly. Build green: 1350 graphitron unit/pipeline tests + sakila-example end-to-end on Java 25. -
FK slot pairing reads the FK’s own keyFields list, not the referenced UK’s own field order (R82 follow-up,
b3c17e6):BuildContext.synthesizeFkJoinpaired slot[i] positionally fromForeignKey.getFields()andForeignKey.getKey().getFields(). The first is the FK’s referencing-column list in declaration order; the second is the referenced UniqueKey’s own declaration order ; the two are parallel only when the FK’s referenced-column ordering happens to match the parent PK’s declaration order. For an FK declared as e.g.FOREIGN KEY (fk_b, fk_c, fk_a) REFERENCES parent (pk_b, pk_c, pk_a)against a parent withPRIMARY KEY (pk_a, pk_b, pk_c), jOOQ’sgetKey().getFields()returns(pk_a, pk_b, pk_c)whilegetKeyFields()returns(pk_b, pk_c, pk_a). ZippinggetFields()against the former produced silent mis-paired slots ; observable asField<Long>.eq(Field<String>)compile errors in generated@splitQueryrows-method JOIN ON predicates downstream when the FK column types are heterogeneous. The fix swapsf.getKey().getFields()forf.getKeyFields()(the FK’s own ordered referenced-column list, parallel togetFields()by jOOQ’s contract) atBuildContext.java:654. Why R82 missed this: R82 lifted slot orientation (which side is source vs target) into a structural fact, but inherited the intra-FK column pairing from jOOQ’s parallel-list contract without revisiting which list the second side reads from. The structural successorJoinSlotOrientationTestconstructs slots directly viaTestFixtures.fkJoinrather than driving them throughsynthesizeFkJoinagainst a real jOOQ FK, and the retired body-string regression tests (splitTableField_listRowsMethod_reorderedHeteroFk_pairsBySqlNameAndType,childInterfaceField_connection_reorderedCompositeFk_pairsBySqlNameAndType) that would have caught it were dropped in favour of orientation-only structural coverage. sakila’s FKs all declare referenced columns in PK declaration order, so the compile-tier safety net atgraphitron-sakila-examplewas silent too. Reproducer fixture:nodeidfixture.reordered_pk_parent(PRIMARY KEY (pk_a bigint, pk_b varchar, pk_c varchar)) plusnodeidfixture.reordered_fk_childwhoseCONSTRAINT reordered_fk_child_parent_fkey FOREIGN KEY (fk_b, fk_c, fk_a) REFERENCES reordered_pk_parent (pk_b, pk_c, pk_a)flips the referenced-column order on the FK side. Heterogeneous types make the regression observable as a Java-class divergence inColumnRef.columnClass;expected "java.lang.String" but was "java.lang.Long"at slot 0 (source=pk_a, target=fk_b) and the symmetric mismatch at slot 2. Tests: newSynthesizeFkJoinReorderedKeysTest(graphitron/src/test/java/no/sikt/graphitron/rewrite/) drivessynthesizeFkJoinagainst the new FK and asserts (a) jOOQ’s two accessors actually diverge on this FK so the test cannot go silent if a future jOOQ release folds them, (b) per-slot type pairing holds, (c)sourceSideColumns()andtargetSideColumns()iterate the FK’s own list. The test fails pre-fix with the slot-0 message above and passes post-fix. Build green:mvn -f graphitron-rewrite/pom.xml install -P!docs -Plocal-dbSUCCESS on Java 25; the fix is one line plus a paired comment naming the trap. -
FK column pairing lifted into typed slots (
fk-column-pairing-typed-slots, R82,2557bf7+82d9313):JoinSlotsealed interface (FkSlotpairs source/target columns;LifterSlotcollapses both onto a single column by construction so DataLoader-key-tuple-IS-target-column-tuple is a type fact, not a prose precondition).JoinStep.FkJoinandJoinStep.LiftedHopcarryList<JoinSlot>; theWithTargetcapability returnsIterable<? extends JoinSlot>fromslots()so positional access (.get(i),.getFirst(),.subList(…)) is a compile error at every consumer.BuildContext.synthesizeFkJoinorients each slot at synthesis time so emitter sites read direction-blind (target.<slot.targetSide()>.eq(source.<slot.sourceSide()>)) regardless of which end of the catalog FK each maps to.MultiTablePolymorphicEmitter.matchingParticipantColretired whole;JoinPathEmitter.emitCorrelationWheredrops itsparentHoldsFkparameter and arity-mismatch throw;InlineTableFieldEmitter,InlineLookupTableFieldEmitter,TypeFetcherGeneratorretire theirparentHoldsFkderivations off cardinality / target-table;SplitRowsMethodEmitter.buildSingleMethod’s if/instanceof FkJoin/LiftedHop block collapses to a single `firstHop.slots()iteration throughWithTarget. Producer@LoadBearingClassifierCheckonWithTarget.sourceSideColumns()/ keyfk-join.slots-oriented-source-and-targetpaired with@DependsOnClassifierCheckon every migrated reader (FieldBuilder ×3, NodeIdLeafResolver, JoinPathEmitter, TypeFetcherGenerator, MultiTablePolymorphicEmitter ×2). Self-ref deviation from spec: spec promised "no signature change, no caller-supplied hint" onsynthesizeFkJoin; the table-name comparison is ambiguous for self-referential FKs (category.parentvscategory.childrennavigate the same FK in opposite directions). The fix threads aselfRefFkOnSourceboolean (derived from list-cardinality at the call site) throughparsePath→parsePathElement→synthesizeFkJoin, consulted only in the self-ref case; non-self-ref FKs ignore it. Tests:JoinSlotOrientationTest(5 model-tier tests pinning slot orientation + theIterable<? extends JoinSlot>compile-time ban);TestFixtures.fkJoin/liftedHophelpers convert nine test fixtures from positional pairs to slot pairs; the two body-string regressions added atfdfec353(splitTableField_listRowsMethod_reorderedHeteroFk_pairsBySqlNameAndType,childInterfaceField_connection_reorderedCompositeFk_pairsBySqlNameAndType) retire in favour of the structural model-tier coverage plus the existing compile-tier check atgraphitron-sakila-example; two execution-tier tests (inlineTableField_selfRef_listCardinality_returnsChildren,inlineTableField_selfRef_nonRootCategory_hasNoChildren) caught the self-ref ambiguity.rewrite-design-principles.adoc:228DTO-parent batching recipe updates to the slot-shaped vocabulary. Follow-ups (not blocking):branchParentFkWhere’s class Javadoc still describes the legacy "FK direction is inferred from the FK’s targetTable" framing ; code is now slot-iterating direction-blind; rewrite to match. `batchedBranchJoinPredicate’s `parentPkColsparameter is dead after the lift (the author flagged this in the Javadoc). The self-refselfRefFkOnSourcethreading is a design fork worth revisiting: a post-synthesis slot-orientation hint onJoinStep.FkJoinset by the field classifier, or a self-ref classifier check that rejects ambiguous schemas at validate time, would localise the disambiguation closer to where SDL semantics live. -
Composite-key NodeId condition args land as typed
Row<N>end-to-end (query-conditions-composite-key-rown-call-site, R79,00ca956+efcf125+fc075b8+0d59ed7+7523497+a428284): the QueryConditions adapter side now hands the composer a typedRow<N><T1, …, TN>(orList<Row<N><…>>) instead of erasing toRowN, applying the typed adapter / composer pairs principle (added inaa66c7e).BodyParam.RowEq/RowIndrop the deadjavaTypeslot;TypeConditionsGenerator.rowTypeNamebuilds the parameter type fromColumnRef.columnClass(), replacingDSL.row(new Field<?>[]{…})with the typedDSL.row(table.c1, …, table.cN)form.ArgCallEmitter’s inline arity > 1 path uses a Java-17-compatible raw-`RecordNpattern + cast to the typedRow<N><…>(parameterizedinstanceofpatterns are JDK 21+); the cast is unchecked at the type-arg level but sound at runtime since the decoder returnsRecord<N><T1, …, TN>. Arity > 22 is rejected upstream as a deferredRejection.structuralinNodeIdLeafResolver.resolvewith wording tracking thevalidateChildConnectionParentPkRow22 precedent. TheQueryConditionsGeneratorshim layer additionally (a) hoists per-class composite-key NodeId decode chains into private static helpers via aCompositeDecodeHelperRegistrydeduplicated by(encoderClass, methodName, mode, list)with namingdecode<NodeType>{Row,Rows}{,OrThrow}, (b) lifts shared outer-arg Maps to one local when ≥2 NestedInputField callParams reference the same outer arg, and (c) reduces thenoCondition()-and chain to a direct return when only one filter applies. Other call sites (Inline*,SplitRows*,TypeFetcher) keep the inline form by passing no registry. Tests: pipeline-tierQueryConditionsPipelineTest(helper-dedup + scalar/list key separation), unitCompositeDecodeHelperRegistryTest(same-key dedup, SKIP↔THROW separation, scalar↔list separation, per-mode body shape ; six tests), unitQueryConditionsGeneratorLiftTest(≥2-share lifts, single does not, distinct outer args do not, cross-filter counts, camelCase naming ; five tests),NodeIdLeafResolverTest.rejects_whenNodeTypeKeyArityExceeds22against a synthetic 23-column PK innodeidfixture, and a sakila compilation-tier regression-guard fixture (filmActorsByCompositeNodeIds+FilmActorCompositeNodeIdFilter) exercising theBodyParam.RowIn→ typedRow<N>path against real jOOQ.TypeConditionsGeneratorTest.nodeIdInFilter_compositeColumns_emitsRowInWithUntypedRowNrenamed and rewritten to assert on the typed form plus aList<Row2<Integer, Integer>>parameter-type sibling assertion. -
Replace string-scan helper-emission gate with
TypeFetcherEmissionContext(type-fetcher-helper-emission-gate, R80,c36734d+c5506e1):TypeFetcherGenerator.generateTypeSpecpreviously decided whether to emit thegraphitronContexthelper by serialising every just-emitted method’sCodeBlockand substring-greping forgraphitronContext(env); an enumerate-or-scan gate whose latest near-miss had silently droppedServiceRecordField(the onlyBatchKeyFieldthat doesn’t extendSqlGeneratingFieldviaTableTargetField). Replaced with a per-classTypeFetcherEmissionContextscratchpad: every emitter that writes agraphitronContext(env)call obtains theCodeBlockthroughctx.graphitronContextCall(), which records the dependency on the way out; class assembly drainsctx.isRequested(GRAPHITRON_CONTEXT)and emits the helper accordingly. ThreadedctxthroughArgCallEmitter(bothbuildCallArgsoverloads, bothbuildMethodBackedCallArgsoverloads,buildArgExtraction),LookupValuesJoinEmitter.buildFetcherBody,SplitRowsMethodEmitter(entry points +emitParentInputAndFkChain),MultiTablePolymorphicEmitter(emitMethods/emitConnectionMethodsoverloads + the four privatebuild*statics), and the in-fileTypeFetcherGenerator.build*privates. Replaced 11 SQL-context literals (graphitronContext(env).getDslContext(env)), the validator pre-step’sgetValidator(env), and the multitable tenant-id data-loader name composition with$Linterpolation ofctx.graphitronContextCall(). Test impact:graphitronContextHelper_emittedForServiceRecordOnlyClasskeeps the helper-presence assertion; the body-string sanity assertion was the test-tier code-string pattern the principles ban, and deletes. Deferred: an architectural review surfaced that the throwawayctxconstructed at three non-Fetchers callers (QueryConditionsGenerator,InlineTableFieldEmitter,InlineLookupTableFieldEmitter) does not record into a context anyone drains, and that@condition(contextArguments: […])does reach those callsites (the comment R80 introduced claiming otherwise was wrong). The closing commitc5506e1corrects the comment; R85 (helper-emission-non-fetcher-hosts) generalisesEmissionContextto the Conditions and Type host classes and adds a sakila compile-tier fixture so the path can’t go latent again. Build green: 1308 graphitron unit/pipeline tests on the fullmvn -f graphitron-rewrite/pom.xml install -Plocal-db. -
R68 Phase 1b: tutorial chapter +
TutorialSmokeTest(diataxis-user-manual, R68,fa36dbc+d0c63c4): six tutorial pages underdocs/manual/tutorial/plus a@QuarkusTestdrift verifier ingraphitron-sakila-example. Prose (fa36dbc):01-prerequisites.adoc(JDK 25, Maven, Docker,git; thedocker run -v init.sql:…one-liner;mvn -f graphitron-rewrite/pom.xml install -Plocal-db;cd graphitron-rewrite/graphitron-sakila-example && mvn quarkus:dev; introspection-curl smoke check);02-first-schema.adoc(the three typesQuery/Customer/Addressand how@table+@fieldmap them to PostgreSQL; honest call-out on@nodeId/@nodebeing out of tutorial scope);03-first-query.adoc({ customers { firstName lastName email } }against the live server; the projection-narrowing claim with the rendered SQL shape; thecustomers(active: true)filter and the@field(name: "ACTIVEBOOL")argument-level mapping that wires it);04-joining-tables.adoc(single-hop@reference(path: [{key: "customer_address_id_fkey"}])with the rendered LEFT JOIN; multi-hopstoreAddresschain throughcustomer.store_id → store.store_id → store.address_id → address.address_id; the inferred-FK shape onStore.customers);05-mutations.adoc(theFilmCreateInput @table(name: "film")+@mutation(typeName: INSERT)shape; theRETURNINGclause keeping the round-trip count to one;UPDATEvia@lookupKeyonfilmId);06-going-further.adoc(the four post-tutorial recipes:add-custom-conditions,connections+sort-results,error-channel,test-your-schema, plus pointers into the rest of the manual).tutorial/index.adocreplaced with a real path overview (cross-link to the example module on GitHub, time budget, before-you-start orientation pointer to Quick Start). Two prose divergences from the plan’s worked-example sketch: (1) the closing query usesaddress { address district }instead ofaddress { addressLine1 }because the example schema doesn’t carryaddressLine1; (2) the "going further" page links toconnections+sort-resultsrather than the plan’s earlierpagination-and-sortingbecause Phase 3 split that recipe into the two shipped pages. Smoke test (d0c63c4):TutorialSmokeTestlives next to the existingGraphqlResourceSmokeTestundergraphitron-sakila-example, reusing theSmokeTestPostgresResourceQuarkusTestResourceLifecycleManager(Testcontainers Postgres orlocal-dbrouting via-Dtest.db.url). Six tests one-per-page-or-query:page1_introspectionVerification({ __typename }returns"Query"),page3_customersBasicSelection(all five customer first names + Mary’s email present),page3_activeFilter(only the threeactivebool: truerows),page4_singleHopReference(theaddress { address district }shape with47 MySakila Drive),page4_multiHopReference(thestoreAddresschain returning both store addresses),page5_createAndUpdateFilm(POSTcreateFilmround-trips afilmId > 5; subsequentupdateFilmagainst that id round-trips the renamed title).@AfterEachDELETE FROM film WHERE film_id > 5keepsApprovalQueryExampleTest’s five-film pin honest. Plus a "Use GraphiQL instead of curl" subsection on `01-prerequisites.adoc(the bundled playground athttp://localhost:8080/graphiql/) and a one-line nudge in03-first-query.adoc’s lede so a reader who skipped the GraphiQL intro still notices the option. Plan deviation: the plan’s "Tests" section called for a separate `tutorial-smoke-testMaven module wrapping a shell script aroundmvn quarkus:devandcurl. The shipped shape is a@QuarkusTestinside the existinggraphitron-sakila-exampletest source. The in-module shape exercises the same JAX-RS endpoint and the sameGraphqlEngine/GraphqlResource/AppContextbean wiring thatmvn quarkus:devwould, runs naturally inside themvn verify -Plocal-dbinvocation that CI already runs from the rewrite reactor (no separate module to wire into the reactor or the docs build), and gives a single-class diff site for future tutorial pages. The drift surfaces the plan named (HTTP endpoint shape, directive existence, query shape) are all covered; "mvn flags" drift is covered by the surrounding rewrite-build itself. Plan markers: Phase 1b heading gains ashipped at fa36dbc + d0c63c4trailer; the deviation is documented inline on the plan page so a future reader doesn’t go hunting for the absent Maven module. Verification:mvn -f graphitron-rewrite/pom.xml -pl graphitron-sakila-example test -Plocal-db250/250 (244 prior + 6 new);ApprovalQueryExampleTestcontinues to pin five films (the smoke test cleans up after itself). The drift-protection seam means a directive disappearing, an endpoint moving, or a generated resolver narrowing differently breaks the corresponding tutorial page before the docs ship. -
R68 Phase 6: cutover quick-start directive pointer to the new manual (
diataxis-user-manual, R68,863d8be):quick-start.adoc:15flipped from the legacygraphitron-codegen-parent/graphitron-java-codegen/README.mdGitHub pointer to the in-treexref:manual/reference/directives/index.adocso readers land on the live, drift-protected directive reference Phase 2 shipped undermanual/reference/directives/. The other legacy-codegen-parent mentions in/docs/are correct as-is and did not move on this commit: fallback notes for features the rewrite stubs (@multitableReference, the polymorphic union pattern), the migration recipe itself (how-to/migrating-from-legacy.adoc), andhow-to/index.adoc’s note that the legacy README’s worked examples remain a useful cross-reference until R26 retires the legacy modules. The legacy README itself is out of AI edit scope (`CLAUDE.md); the one-paragraph "moved to graphitron.sikt.no/manual/reference/" stub redirect is the Sikt-maintainer companion commit the original Phase 6 body called out, lives outside this plan, and remains the gating step before R26 retires the legacy modules. Plan markers: Phase 6 heading gains ashipped at 863d8betrailer; the body extends with the as-shipped scope (which legacy mentions moved, which stayed) so a future reader can audit the cutover surface without re-grepping/docs/. -
R68 Phase 4 second half: explanation chapter (
diataxis-user-manual, R68,1ea0855): six new pages underdocs/manual/explanation/plus an updated index mapping them. Architectural framing:why-database-first.adoc(the database is the source of truth; the GraphQL layer is a typed view; cross-links tographitron-principles.adoc) andwhy-jooq-and-graphql-java.adoc(jOOQ is where you work, GraphQL-Java is under the hood, the dependency triple constrains and enables; cross-links todependencies.adoc). Pipeline framing:how-it-works.adoc(the build-time and request-time paths in 30 seconds, named call-outs for parse/classify/validate/emit and graphql-java/DataFetcher/jOOQ/DataLoader at runtime),classifier-mental-model.adoc(the(parent context, return type)two-axis model in user terms; concrete walk-through of aCustomerschema showing same-scope join,@splitQuery-driven batch, and@externalFieldcomputed field side by side; the unknown-name-with-candidate-hint and directive-conflict rejection shapes),batching-model.adoc(same-scope joins versus cross-scope batches;@splitQueryopens a new scope keyed by parent PK;@lookupKeyparameterises a derived target table; the N × M contract that custom@conditionmethods must respect; the per-requestDataLoaderRegistrylifecycle). "Why does it work that way" answers:design-decisions.adoccovers seven user-visible constraints with their rationale (why@conditionmethods take a table parameter even when not aliased; why@lookupKeyblocks pagination; why mutations require@tableon the input type rather than reusing the output’s binding; why selection drives projection; why federation_entitiesis a@lookupKeyshape; why the validator’s diagnostics surface is closed-set; why build-time wins over runtime introspection). All pages cross-link into the directive reference, the diagnostics glossary, the how-to recipes, and (where appropriate) the architecture chapter and the existing top-level explanation pages. No verifier: explanation prose is curated voice, not a surface that drifts mechanically against the code; the directive reference (DirectiveDocCoverageTest), Mojo reference (MojoDocCoverageTest), diagnostics glossary (DiagnosticsDocCoverageTest), and deprecations index (DeprecationsDocCoverageTest) carry the bidirectional drift-protection seams. Plan markers: Phase 4 trailer extended to "Shipped at868593a(runtime-api),d796c4c(mojo + verifier),<TBD>(explanation)"; explanation half no longer outstanding. AsciiDoctor build green: all six pages render without warnings; full site build succeeds. -
R68 Phase 4 second half: explanation chapter (
diataxis-user-manual, R68,1ea0855): six new pages underdocs/manual/explanation/plus an updated index mapping them. Architectural framing:why-database-first.adoc(the database is the source of truth; the GraphQL layer is a typed view; cross-links tographitron-principles.adoc) andwhy-jooq-and-graphql-java.adoc(jOOQ is where you work, GraphQL-Java is under the hood, the dependency triple constrains and enables; cross-links todependencies.adoc). Pipeline framing:how-it-works.adoc(the build-time and request-time paths in 30 seconds, named call-outs for parse/classify/validate/emit and graphql-java/DataFetcher/jOOQ/DataLoader at runtime),classifier-mental-model.adoc(the(parent context, return type)two-axis model in user terms; concrete walk-through of aCustomerschema showing same-scope join,@splitQuery-driven batch, and@externalFieldcomputed field side by side; the unknown-name-with-candidate-hint and directive-conflict rejection shapes),batching-model.adoc(same-scope joins versus cross-scope batches;@splitQueryopens a new scope keyed by parent PK;@lookupKeyparameterises a derived target table; the N × M contract that custom@conditionmethods must respect; the per-requestDataLoaderRegistrylifecycle). "Why does it work that way" answers:design-decisions.adoccovers seven user-visible constraints with their rationale (why@conditionmethods take a table parameter even when not aliased; why@lookupKeyblocks pagination; why mutations require@tableon the input type rather than reusing the output’s binding; why selection drives projection; why federation_entitiesis a@lookupKeyshape; why the validator’s diagnostics surface is closed-set; why build-time wins over runtime introspection). All pages cross-link into the directive reference, the diagnostics glossary, the how-to recipes, and (where appropriate) the architecture chapter and the existing top-level explanation pages. No verifier: explanation prose is curated voice, not a surface that drifts mechanically against the code; the directive reference (DirectiveDocCoverageTest), Mojo reference (MojoDocCoverageTest), diagnostics glossary (DiagnosticsDocCoverageTest), and deprecations index (DeprecationsDocCoverageTest) carry the bidirectional drift-protection seams. Plan markers: Phase 4 trailer extended to "Shipped at868593a(runtime-api),d796c4c(mojo + verifier),<TBD>(explanation)"; explanation half no longer outstanding. AsciiDoctor build green: all six pages render without warnings; full site build succeeds. -
R68 Phase 5 closing slice: deprecations index +
DeprecationsDocCoverageTest(diataxis-user-manual, R68,23c2056): aggregator pagereference/deprecations.adoclists every SDL@deprecated()marker indirectives.graphqls(currently@asConnection(connectionName:)andExternalCodeReference.name) plus the one whole-directive deprecation that the GraphQL spec disallows@deprecatedon (@index, the legacy alias for@order(index:)). Each row points at the canonical directive reference page for the migration prose (asConnection.adoc,record.adoc,index.adoc); a separate "Rejected, not deprecated" section calls out@notGeneratedso authors don’t conflate the categories. Two honest deviations from the plan are documented inline on the page itself: (1) the source of truth is the SDL@deprecated()marker, not Java@Deprecatedannotations on a "directive-classification model" (the legacy classification model lives in the out-of-AI-scope legacy modules); (2) the plan’s "target removal version" column is dropped because the rewrite’s@deprecated(reason:)markers do not carry a structured removal version and there is no separate directive-surface versioning cadence to anchor it on. Verifier:DeprecationsDocCoverageTestextracts qualified<parent>.<member>keys from the SDL by walking backwards from each@deprecatedhit to the closestdirective @<name>orinput <Name>declaration, then asserts every key’s two halves both appear in the doc page (rows naturally include both, e.g.@asConnection(connectionName:)andExternalCodeReference.name). Whole-directive deprecations are covered via a smallWHOLE_DIRECTIVE_DEPRECATIONSallow-list (currentlySet.of("index")) that a separate test arm asserts against the doc; adding a new whole-directive deprecation requires adding to both the allow-list and the doc, mirroring the bidirectional drift-protection shape ofDirectiveDocCoverageTest. The reference index gains a "Deprecations" section pointing at the new page. Plan markers updated: Phase 5 marked shipped on the heading; Phase 4 gains a "shipped at868593a(runtime-api) andd796c4c(mojo + verifier)" trailer with explanation pages explicitly noted as outstanding (the second-half Phase 4 slice). Build green: 258 sakila-example tests pass on Java 25. -
Multi-schema
@tablerejection: structured ambiguity message + R68 docs delta (R78 follow-up,41636e9+36238b0): closes the "Open follow-ups" line on R78’s changelog entry by landing both the documentation delta R78 deferred and the user-facing rejection R78’s spec quoted but R78 didn’t actually wire. NewBuildContext.unknownTableRejection(String sqlName)helper consolidates the@table(name:)rejection-construction decision: branches onJooqCatalog.findCandidateSchemasFor(size >= 2emits a structural ambiguity message naming the colliding schemas plus inline qualified-form suggestions; otherwise falls through to the existingRejection.unknownTablewith the Levenshtein-ranked candidate hint, covering missing names, qualified misses, and the degenerate single-schema-with-no-Tables-class case). The three@table-directive sites inTypeBuilder(buildTableType,buildTableInterfaceType,buildTableInputType) all route through it, so the better message reaches authors at every directive site that resolves aname:argument. The eight non-@table-directiveresolveTablecallsites (one inNodeIdLeafResolver, four inBuildContextresolving table names from FK metadata or other classifier-internal sources) keep the existing rejection shape since@table(name:)framing doesn’t fit those error contexts. Docs:reference/directives/table.adocgrows a sentence undername:describing the qualified form and a constraint bullet quoting the new rejection verbatim;how-to/map-types-to-tables.adocgrows a "Tables in non-default schemas" subsection (three SDL snippets covering unique-across-schemas + two qualified-form schemas plus a paragraph on the failure modes); the diataxis-user-manual plan’s "Pending content additions" section flips from "Backlog candidate" to "done" inline. Tests: three new cases inJooqCatalogMultiSchemaTestpin the three branches against the multischema_a/multischema_b fixture (ambiguous unqualified, missing unqualified, qualified miss); construction passesnullschema andnullctx since the helper only touches the catalog andBuildContext.buildTypeNamesByTableKeyalready null-guards. 1307 graphitron + 256 sakila-example + 48 graphitron-maven tests green. -
Typed jOOQ class references for multi-schema correctness (
jooq-multi-schema-typed-references, R78,b334036+6add327+9e417b5+4a61223+ab64ad0+64405d6): fixes a latent multi-schema generated-code bug (imports emitted as<jooqPackage>.tables.X, dropping the schema segment) and a parallel silent-wrong-schema resolution bug (JooqCatalog.findTablepicked whichever schema iterated first on a name collision) by replacing string concatenation againstString jooqPackagewith javapoetClassNamevalues populated once at parse time fromTable<?>reflection. Model:TableRefcarriestableClass(the<schemaPackage>.tables.<X>class),recordClass(the<X>Recordclass viaTable.getRecordType()), andconstantsClass(the schema’sTablesclass) asClassName; the priorString javaClassNamefield is gone. NewForeignKeyRef(sqlName, keysClass, constantName)replacesString fkJavaConstantonJoinStep.FkJoin; theKeyshost class is the FK-holder schema’s so cross-schema FKs join correctly without per-emitter schema arithmetic. Catalog API:JooqCatalog.TableEntryexposestableClass(),recordClass(),constantsClass()typed accessors plustoTableRef(sqlName)as the single factory, collapsingBuildContext.resolveTableandServiceCatalog.buildTableRef(which already returnedOptional<TableRef>);findForeignKeyByName(sqlConstraintName): Optional<ForeignKeyRef>replacesfkJavaConstantName(): Optional<String>. Catalog-miss is structural:BuildContext.resolveTablereturnsOptional<TableRef>andsynthesizeFkJoinreturnsOptional<FkJoin>; consumers route absence throughUnclassifiedType/UnclassifiedFieldrather than empty-string sentinels, so emit sites never see a partial ref. Resolution-time disambiguation: newparseQualifiedTableName(String)plus twofindTableshapes (findTable(qualifiedName)andfindTable(schemaSqlName, tableSqlName)); unqualified@table(name: "x")resolves iff exactly one schema containsx, collisions reject with"@table(name: 'film') is ambiguous: defined in schemas [public, archive]; qualify as 'public.film' or 'archive.film'", qualified@table(name: "schema.x")scopes to the named schema. The eight directive-parsing sites (three inBuildContext, four inTypeBuilder, one inNodeIdLeafResolver) all participate; directive SDL signature is unchanged.jooqPackagesurvivors: onlyJooqCatalog’s constructor (loads `<jooqPackage>.DefaultCatalogreflectively) andCatalogBuilder’s filesystem-path conversion (`replace('.', '/')); no emitter takes ajooqPackageparameter, no concrete+ ".tables"/+ ".tables.records"/ClassName.get(jooqPackage, "Tables" | "Keys")concatenation survives anywhere ingraphitron/src/main/.NodeIdEncoderClassGeneratorreadsnt.table().constantsClass()perNodeTypeinstead of synthesising one fromjooqPackage;QueryNodeFetcherClassGenerator.generatedrops a deadjooqPackageparameter;EntityFetcherDispatchClassGeneratorandSelectMethodBodyswitch toentity.table().tableClass();GeneratorUtils.ResolvedTableNamescollapses to a singletypeClassfield with the other two reading fromTableRef; the privatebuildRowKey,buildAccessorKey{Single,Many},buildKeyExtraction{,WithNullCheck}helpers become parameter-pure onjooqPackage. Test surface: newmultischema_a+multischema_bjOOQ-codegen fixture ingraphitron-sakila-dbwith cross-schema FK (gadget → widget), a sharedeventtable in both schemas, and a uniquewidget/gadgetper schema;JooqCatalogMultiSchemaTestasserts cross-schematableClass/recordClassFQNs (multischema_a.tables.Widget,multischema_a.tables.records.WidgetRecord), the FK-holderKeysclass for cross-schema traversal (multischema_b.Keys), schema-qualified resolution, unqualified-and-unique resolution, and the empty-on-ambiguity policy for the sharedeventname;TestFixtures.tableRef(…)helper centralises the newClassNameconstruction across the 107 test sites. Phasing: Phase 1 fixture landed first to turn the compilation tier red; Phase 2 (catalog API +TableRefmigration + qualified-name resolver) was the load-bearing change; Phase 3 (ForeignKeyRef+ record-class typed refs) and the hotfix sweep finished emit-side cleanup; Phase 4 dropped the deadjooqPackageparameter threading uncovered by the post-migration audit, closing the boundary on both sides (no concatenation in bodies, no parameter on signatures). Open follow-ups: docs delta for the@table(name: "schema.table")syntax (folded into R68’s## Pending content additionsfor next-touch-oftable.adocandmap-types-to-tables.adoc); execution-tier coverage for cross-schema FK joins at runtime (Backlog stub); LSP directive-validation pass for the dotted syntax (one-line check, not a hard R78 requirement). Build green:mvn -f graphitron-rewrite/pom.xml install -Plocal-dbSUCCESS on Java 25. -
TableRecord-keyed Map returns on
@servicerows methods (service-rows-tablerecord-key-shape, R70): closes R32’s deferred "element-shape conversion when the developer’sSourcesisSet<TableRecord>/List<TableRecord>`" bullet by extending the variant taxonomy rather than threading conversion through the emitter. Two new `BatchKey.ParentKeyedpermits ;TableRecordKeyed(parentKeyColumns, elementClass)andMappedTableRecordKeyed(parentKeyColumns, elementClass); carry the developer-declaredClass<? extends TableRecord<?>>on the variant;keyElementType()returns it directly.ServiceCatalog.classifySourcesType’s `TableRecordelement branch reroutes onto the new permits, threading the typed class. Three downstream sites widen theirisMappedinstanceof checks to includeMappedTableRecordKeyed(RowsMethodShape.outerRowsReturnType,TypeFetcherGenerator.buildServiceDataFetcher,TypeFetcherGenerator.buildServiceRowsMethod).GeneratorUtils.buildKeyExtraction’s sealed switch grows one arm emitting `Record) env.getSource(.into(Tables.X)withTables.Xresolved from the parent table; the rows-method emitter’sreturn ServiceClass.method(keys)line covers the new variants by construction (the lambda’skeyslocal is already typedSet<X>/List<X>, the developer’s signature matches, the call type-checks). The deferred-conversion comment onbuildServiceRowsMethoddrops out. Resolver-side parent-table consistency check:ServiceDirectiveResolver.validateTableRecordSourceParentTablerejectsSet<X>against a parent whose record class isn’tXwith a candidate-hint pointer; without it the typedinto(Tables.X)projection on a wrong-typed parent would silently produce nonsense. New helperBuildContext.recordClassForTypeName(parentTypeName)reads the@tabledirective on the parent type and looks up the catalog.MappedRowKeyed/RowKeyeddocstrings tighten to "onlySet<RowN<…>>/List<RowN<…>>classify here"; the variants are now shape-pure, matchingRecordKeyed/MappedRecordKeyedsiblings. Tests:BatchKeyTestextends the per-variant shape map with the two new permits (keyElementType()returnsFilmRecord,javaTypeName()yieldsjava.util.{List,Set}<…FilmRecord>);ServiceCatalogTest’s two existing `TableRecordcells flip fromRowKeyed/MappedRowKeyedtoTableRecordKeyed/MappedTableRecordKeyed. L5 + L6: newFilm.titleTitlecaseschema field paired withFilmService.titleTitlecase(Set<FilmRecord>) → Map<FilmRecord, String>exercises the typed-record path end-to-end against the sakila PostgreSQL fixture;GraphQLQueryTest.films_titleTitlecase_resolvesViaServiceRecordFieldDataLoader_tableRecordSourceruns{ films { title titleTitlecase } }and asserts each title round-trips through the typed-record extraction. Builds on R61 (variant identity tracks shape;Record.into(Table)projection at the parent-key extraction site, structurally identical toRecordKeyed/MappedRecordKeyedarms but typed to the developer’s element class). Open follow-ups (deferred): single-cardinality typed-record positional signature (X method(X parent)driven byLoaderDispatch.LOAD_ONE) ; confirm if the sameTableRecordKeyedpermit covers it cleanly when a real schema needs it; custom-scalarV-types in the typed-record map inheritRowsMethodShape.strictPerKeyType’s null-skip arm until R45 lands. Post-landing addition (`c6a10133): composite-PK regression-guard cellServiceCatalogTest.reflectServiceMethod_compositeKeyTableRecordSources_classifiedAsMappedTableRecordKeyedpinningSet<FilmActorRecord>(2-PK) ontoMappedTableRecordKeyedso a future classifier collapse ontoMappedRowKeyedis caught at L1 rather than at consumer-build time; mirrors the consumer’sSet<KvotesporsmalRecord>3-PK shape. Editorial follow-ups noted at approval (not blocking): approval pass on 2026-05-08 (4 days after In Review) confirmed all R70 invariants survive substantial post-landing drift through R102 (BatchKey invariants + record components + validator generalisation), R77 (bulk DML emit), R82 (slot lift), R78 (jOOQ multi-schema typed references ;parentTableextraction now resolves throughTableRef.constantsClass()), R104 (TypeRegistry / FieldRegistry), and R114 (multi-hop@reference); theRecord) env.getSource(.into(Tables.X)extraction continues to compile and run the L6 sakila path. Duplicate-key DataLoader behaviour withTableRecordkeys is structurally sound (the mapped DataLoader factory routes through the same hashing path that already works forRecordNkeys, andorg.jooq.impl.AbstractRecord.equals/hashCodeis value-array-based) but the L6 fixture exercises 5 unique films so the de-duplication path is not directly asserted; worth a follow-up sibling if a regression ever surfaces. Build green:mvn install -Plocal-dbSUCCESS on Java 25; 1268 unit + pipeline tests pass; 186GraphQLQueryTestexecution tests pass. -
selection/parser audit (selection-parser-audit, R30): audit found the parser IS needed.@experimental_constructType(selection: "…")carries a generation-time string argument; graphql-java’sDataFetchingFieldSelectionSet/SelectedFieldAPIs only exist inside a live query execution and cannot substitute. Theselection/package stays; wiring it into the@experimental_constructTypeclassifier is tracked separately. -
Promote
graphitron-testtographitron-sakila-example(rename, Quarkus runtime, consumer test pattern) (rewrite-example-quarkus-jaxrs, R67,4c2dc5d+b869b6e+e5314e9+4af7001): turns the rewrite’s internal end-to-end test module into a public-facing artifact that doubles as the runnable reference application and the recommended consumer test pattern, closing both the docs gap (docs/quick-start.adoc:21,64no longer points at the legacygraphitron-exampleon the retiredgraphitron-servletruntime) and the absence of a documented "how do I test my schema" answer for rewrite consumers. Stage 0 (4c2dc5d) splitsgraphitron-fixturesintographitron-sakila-db(catalog + jOOQ codegen) andgraphitron-sakila-service(Java service fixtures); renamesgraphitron-testtographitron-sakila-example; relocates the tier-annotation package (@UnitTier/@PipelineTier/@CompilationTier/@ExecutionTierunderno.sikt.graphitron.rewrite.test.tier) fromgraphitron-fixtures’s main source root into `graphitron’s test source root, republished as a `teststest-jar viamaven-jar-plugin’s `test-jargoal so import paths stay stable across the rename; updates every dependent (graphitron,graphitron-lsp, the twographitron-mavenITs,CLAUDE.md,.claude/web-environment.md,graphitron-rewrite/docs/{README,testing,rewrite-design-principles}.adoc, six javadoc/code comments,SampleQueryServiceandMutationPayloadLifterTestjavadoc). Stage 1 (b869b6e) layers Quarkus 3.34.5 + JAX-RS ontographitron-sakila-example: importsquarkus-bom, drops the test-scopehibernate-validator+expresslypair for compile-scopequarkus-hibernate-validator, addsquarkus-rest,quarkus-rest-jackson,quarkus-config-yaml,quarkus-jdbc-postgresql,quarkus-agroal, thequarkus-junit5+rest-assuredtest pair, and thequarkus-maven-pluginexecution. Hand-written runtime underapp/:GraphqlEngine(@ApplicationScoped, builds the schema once viaGraphitron.buildSchema(b → {})),GraphqlResource(@Path("/graphql"), POSTapplication/json→application/graphql-response+jsonper the GraphQL-over-HTTP spec, GET for query-only, freshDataLoaderRegistryandAppContextper request stashed underGraphitronContext.classonExecutionInput),AppContext(implements GraphitronContext, per-requestDSLContextfrom the Quarkus-managedAgroalDataSourceplus a context-values map fed intogetContextArgument).application.yamlconfigures HTTP port and JDBC datasource via${VAR:default}env-var defaults pointing at thelocal-dbPostgres. One smoke test (GraphqlResourceSmokeTest+SmokeTestPostgresResourceQuarkusTestResourceLifecycleManager) POSTs{ customers { firstName } }and asserts 200 + non-empty; in-process query-to-database tests run unchanged alongside it.default-compilepinned to<release>17</release>to keep the consumer-runtime app code under the rewrite’s Java-17 generated-output guarantee. Stage 2 (e5314e9) curates the test surface: 11 existing tests split intosrc/test/java/…/querydb/(the four query-to-database testsGraphQLQueryTest,FederationEntitiesDispatchTest,FederationBuildSmokeTest,NoFederationRegressionTest) andinternal/(the seven generator-internal tests).IdempotentWriterTestrelocates fromgraphitron/src/test/java/no/sikt/graphitron/rewrite/intographitron-sakila-example/…/internal/with explicit imports forRewriteContextandGraphQLRewriteGeneratorsince the package-relative resolution no longer works after the move. Two new worked examples land underquerydb/:MatchQueryExampleTest(load.graphql, execute, assert specific paths) +customers_basic.graphql, andApprovalQueryExampleTest+films_basic.{graphql,approved.json}(execute, serialise to canonical JSON, compare; on divergence write a sibling.actual.jsonso the next iteration is "diff the two; mv onto approved if intentional").README.mdlands at the module root: opens with the two roles (runnable reference, recommended test pattern), tables which directories to copy for each role, walks through the runtime files, names the two test patterns plus the carve-out forinternal/("you do not need to copy anything frominternal/`"). Stage 3 (`4af7001) repointsdocs/quick-start.adoc:21,64atgraphitron-sakila-example; the "Working example" section grows a one-paragraph mention that the same module doubles as the recommended consumer test pattern, with a link to its README;graphitron-rewrite/docs/getting-started.adoc"Hello world" gains a one-line pointer at the example module after the per-request-context worked example. Verification:mvn -f graphitron-rewrite/pom.xml install -Plocal-dbbuilds clean on Java 25 with the example module’s main jar compiled under Java 17; all 1643 tests pass (244 ingraphitron-sakila-example);mvn quarkus:devfrom the example module boots and serves real Sakila customer rows over HTTP. Unblocks R26 (retire-maven-plugin) on the docs-pointing side and lays the public-facing artifact R68 (diataxis-user-manual) anchors its tutorial chapter andtest-your-schema.adochow-to on. Out of scope and explicitly deferred: deleting legacygraphitron-example/(R26 owns that gating); HTTP-shaped query-to-database tests (the in-process pattern viagraphql-javastays canonical; the smoke test is the only HTTP-shaped check in the module); pedagogical schema simplification (getting-started.adocremains the on-ramp); test-pattern variants beyond approval + match (richer taxonomies are future follow-ups). -
Make the typed
Rejectionhierarchy load-bearing across producers (lift-unclassified-field-onto-sealed-result, R58,7c10226+09541ed+5d29a3d+68a062c+83816e0+3dcd3c6): replaces the flat(RejectionKind kind, String reason)pair onUnclassifiedField/UnclassifiedType/ValidationErrorwith the sealedRejectionhierarchy and threads the typed shape from every producer site through every consumer. Top-levelAuthorError | InvalidSchema | Deferred, sub-sealedAuthorError.{UnknownName | Structural}andInvalidSchema.{DirectiveConflict | Structural},StubKey.{VariantClass(@Nullable Class<? extends GraphitronField>) | EmitBlock(EmitBlockReason)}, with a self-containedcandidateHintrenderer so the model package can render rejection messages without pulling inBuildContext.RejectionKindsurvives as a derived projection (RejectionKind.of(Rejection)) for the[<kind>] <message>log surface. Phase 0 (7c10226) dropsRejectionKind.INTERNAL_INVARIANT; the single producer atFieldBuilder.classifyChildFieldOnTableType’s nested-fields fallthrough becomes an `AssertionError. Phase A (09541ed) introduces the seal and liftsUnclassifiedFieldto carryRejection rejection; every classifierResolved.Rejectedarm widens. Phase B (5d29a3d) mirrors the lift ontoUnclassifiedType; 24 sites (21 inTypeBuilder, 3 inEntityResolutionBuilder); three table-resolution sites constructAuthorError.UnknownNameviaRejection.unknownTable. Phase C (68a062c) renamesTypeFetcherGenerator.NOT_IMPLEMENTED_REASONStoSTUBBED_VARIANTS(Map<Class, Rejection.Deferred>); the fourSplitRowsMethodEmitter.unsupportedReasonoverloads collapse ontoOptional<Rejection.Deferred>keyed byEmitBlockReason. Phase D (83816e0) walks the direct candidate-hint producers onto typedAuthorError.UnknownNamefactories (BatchKeyLifterDirectiveResolver,ServiceCatalogvia wideningServiceReflectionResult.failureReason: String → rejection: Rejection,FieldBuilderfor@nodeId(typeName:)/ column-on-FK-resolved-table / scalar-column-miss /DmlKindResult.Unknown); adds factoriesunknownTypeName,unknownEnumConstant,unknownNodeIdKeyColumn,unknownDmlKindand the leaf-armprefixedWith(String)instance method (used by the four wrapper sites that thread caller-specific prose ontoServiceReflectionResult.rejection); drops unusedAttemptKind.{TABLE_METHOD, ARGUMENT_NAME, FIELD_NAME}. Phases E–I (3dcd3c6): E replaces the nested-rewrap switch inFieldBuilder.classifyChildFieldOnTableTypewith a singleunc.rejection().prefixedWith(parentPrefix)call so the inner variant’s typed components survive the rewrap (an LSP fix-it on a nested column miss no longer has to re-derive candidates by re-running the classifier). F liftsdetectChildFieldConflict,detectQueryFieldConflict, anddetectTypeDirectiveConflictfromStringtoRejection.InvalidSchema.DirectiveConflict; migrates explicit conflict sites (@service`@mutation`, `@notGenerated`, `@asConnection`@splitQuery,@asConnection`@lookupKey` 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`@mutationand@table+@record), theConditionJoinReportablecapability seal, and the validator-sideUnknownNamesurvival throughprefixedWithontoValidationError;RejectionRenderingTestextended with 8 model-tier cases for the new factories andprefixedWithpreservation across every sealed leaf. Out of scope and tracked separately: deeper carrier widenings whose producers Phase D could not migrate without changing intermediate carriers (ParsedPath.errorMessage,InputFieldResolution.Unresolved.reason,ArgumentRef.ScalarArg.UnboundArg.reason,EnumMappingResolver.EnumValidation.Mismatchjoined-prose aggregation,TypeBuilder.keyColumnErrors/failuresaggregation) tracked under R66 (rejection-string-carrier-widening); LSP fix-its consumingAuthorError.UnknownName.candidatesare R18; threading nested rejection chains as a typedRejection.NestedRejectarm deferred until error-aggregation consumers (LSP, watch-mode) demand it;ArgumentRef.UnclassifiedArg.reasonandBuildWarning.messagelifts (separate axes, single producers);RejectionKindrename. Build green:mvn -f graphitron-rewrite/pom.xml install -Plocal-dbSUCCESS on Java 25. -
Tighten accessor-derived BatchKey model and emitter coordination (
accessor-batchkey-emitter-tightening, R65,b0c6846): six independent architectural cleanups surfaced during the R60 reviewer pass, all landed as one commit. (1) DropAccessorRowKeyedMany.Container: enum + record component gone;GeneratorUtils.buildAccessorRowKeyMany’s for-loop iterates any `Iterableso the LIST/SET split was never load-bearing. TheSet<X>vsList<X>parent-class declaration is still exercised by the two pipeline-tier fixtures (ListPayload,SetPayload); the variant just stops preserving which side it came from.FieldBuilder.AccessorMatch.Manyno longer carries the container;BatchKeyTestand theACCESSOR_ROWKEYED_MANY_*_ACCESSORpipeline cases dropped the enum-pinning assertions. (2)BatchKeyField#emitsSingleRecordPerKey()capability: new default method onBatchKeyFieldreturningfalse; overridden onSplitTableField(!returnType().wrapper().isList()) andRecordTableField(batchKey() instanceof AccessorRowKeyedMany). The two consumer sites ;TypeFetcherGenerator’s `scatterSingleByIdxhelper-emission gate andSplitRowsMethodEmitter.buildForRecordTable’s `buildSingleMethodrouting ; both fold onto the capability, so a future variant whose rows-method emits 1 record per key implements the capability without adding a third disjunct at either site. (3)RecordParentBatchKey#preludeKeyColumns()capability + prelude param tightening: new abstract method;RowKeyeddelegates toparentKeyColumns(), the three target-side arms (LifterRowKeyed,AccessorRowKeyedSingle,AccessorRowKeyedMany) delegate totargetKeyColumns()viahop.targetColumns().SplitRowsMethodEmitter.emitParentInputAndFkChain’s prelude param tightened from `BatchKeytoRecordParentBatchKey; thepkColsswitch withdefault → throwcollapsed tobatchKey.preludeKeyColumns(). Helper-method chain (buildListMethod/buildSingleMethod/buildConnectionMethod) tightened to match. To carry the chain end-to-end,SplitTableField.batchKey()andSplitLookupTableField.batchKey()tightened fromBatchKey.ParentKeyedtoBatchKey.RowKeyed(which already implements bothParentKeyedandRecordParentBatchKey);deriveSplitQueryBatchKeyreturn type matches. The two@DependsOnClassifierCheckannotations on the prelude collapsed into one (the JOIN-on side claim aboutLiftedHop); the BatchKey-side claim is now load-bearing in the type system.TypeClassGenerator.collectBatchKeyColumns’s redundant `instanceof BatchKey.RowKeyedchecks became direct accessor reads onstf.batchKey().parentKeyColumns(). (4) Container/element classifier walk lifted intoServiceCatalog: newServiceCatalog.ContainerKind { SINGLE, LIST, SET }enum +ContainerSplitrecord +peelContainer(Type, Set<ContainerKind>)helper.classifySourcesType(SOURCES path) acceptsLIST | SET;FieldBuilder.classifyAccessorReturn(accessor path) accepts all three. Element-class checking (jOOQTableRecordsubtype, orRowN/RecordNparameterised raw on the SOURCES path) stays per-caller. Both call sites remain inside parse-boundary classes; the shape walk has one home. (5) TypedLoaderDispatchprojection: newBatchKey.LoaderDispatch { LOAD_ONE, LOAD_MANY }enum andRecordParentBatchKey#dispatch()accessor; the three single-key arms returnLOAD_ONE,AccessorRowKeyedManyreturnsLOAD_MANY.TypeFetcherGenerator.buildRecordBasedDataFetcherreadsbatchKey.dispatch()once and forks the loader value type and dispatch call shape on the projection (replacing the inlineinstanceof AccessorRowKeyedMany). The@DependsOnClassifierCheckannotation onbuildRecordBasedDataFetcherwas rewritten to reference thedispatch == LOAD_MANYrule; the producer-side description onaccessor-rowkey-cardinality-matches-fieldwas tightened in the same pass to drop the obsoleteusesLoadManyterm. (6) Delete unusedListAccessorOnSingleFieldfixture: the record had javadoc noting it existed "for symmetry"; no test referenced it. The unusedFilmActorRecordimport dropped with it. Deviations from spec: (a) Item 5 implemented as enum rather than the spec’s proposed sealedLoaderDispatch { LoadOne | LoadMany }since both arms carry no per-arm data and consumers fork on identity, not on captured fields (per the design principle "When variants carry different data, use a sealed interface; an enum forces every variant to have the same shape" ; both arms share the empty shape). (b) Item 3’sSplitTableField/SplitLookupTableFieldbatchKey()tightening was not explicitly called out by the spec, but proved necessary to type the prelude parameter asRecordParentBatchKeyend-to-end (the alternative was a runtime cast at the call site); type-only narrowing, no behavioural change. (c) Items 2 and 5 capabilities kept separate rather than collapsed:emitsSingleRecordPerKeyis aBatchKeyField-level question (rows-method shape; depends on field cardinality);dispatchis aRecordParentBatchKey-level question (loader call shape). They coincide forRecordTableFieldwithAccessorRowKeyedMany(bothtrue/LOAD_MANY) but diverge for single-cardinalitySplitTableField(emitsSingleRecordPerKey == true, nodispatchprojection ;SplitTableFieldcarriesRowKeyedwhosedispatch()isLOAD_ONEregardless of field cardinality). Verification:mvn -f graphitron-rewrite/pom.xml install -Plocal-dbSUCCESS on Java 25; 1262 unit + pipeline tests pass; 238 graphitron-test compilation + execution tier tests pass;LoadBearingGuaranteeAuditTestno orphans. Out of scope (unchanged): renamingAccessorRowKeyedSingle/AccessorRowKeyedMany(names accurately reflect cardinality at the variant level); theSinglepermit’s emitter wiring (already complete in R60; execution-tier coverage gap tracked under the validator’s Invariant #10 lift);RecordBatchKeyResolutionand theAccessorDerivation/AccessorMatchtwo-stage builder hierarchy (clean applications of "Builder-step results are sealed"). -
@servicerows-method body ; strict return-type validation + shape lift to model (service-rows-method-body, R32,64b8e2c+e28540b+83bcfdf): closes out R32 by mirroringServiceCatalog.reflectServiceMethod’s strict-return check on the child `@servicepath and resolving the (returnType,BatchKey) →Map<K, V>/List<List<V>>/List<V>cross-product once at the model layer. Iteration 1 (the body emission, separately captured below asbefc156) shipped earlier; this entry covers iterations 2 and 3 plus a review-pass nit. Iteration 2 ; strict child-@servicevalidation:ServiceDirectiveResolver.validateChildServiceReturnTyperejects developer methods whose declared return type doesn’t structurally match the rows-method’s outer shape. Per-keyVderives fromReturnTypeRef; raworg.jooq.RecordforTableBoundReturnType, the backing class forResultReturnTypewith non-nullfqClassName, the standard Java type for the five standard GraphQL scalars (String/Boolean/Int/Float/ID); other cases (custom scalars, enums,PolymorphicReturnType,ResultReturnTypewith no backing class) skip the strict check. Carries theservice-directive-resolver-strict-child-service-return@LoadBearingClassifierCheckkey, paired with@DependsOnClassifierCheckonTypeFetcherGenerator.buildServiceRowsMethod; the emitter can now emitreturn ServiceClass.method(<args>);against a structurally-typed return without a defensive cast or wildcard local. Author errors surface at classify time rather than asjavacerrors on the generated source. Iteration 3 ; lift rows-method shape onto the model: validator and emitter each used to reconstructMap<K, V>/List<List<V>>/List<V>from(returnType, batchKey)independently, the per-keyVderivation lived a third time onChildField.ServiceRecordField.elementType()with a deliberately-divergent fallback, andGeneratorUtils.keyElementTypehad been bumped topublicso the classifier-tier validator could import from the generators package. The shared form lives in two new model-package surfaces:BatchKey.keyElementType()(adefaultaccessor on the sealed root, replacing the static helper inGeneratorUtils) andRowsMethodShape.{strictPerKeyType, outerRowsReturnType, standardScalarJavaType}(the per-keyVdecision and the(isMapped, isList)outer-shape construction). Validator and emitter both callRowsMethodShape.outerRowsReturnType(perKey, returnType, batchKey); only theperKeyinput differs (validator:RowsMethodShape.strictPerKeyTypeand skip on null; emitter: the field-knownVfrom the literalRECORDconstant orsrf.elementType()). The@LoadBearingClassifierCheck/@DependsOnClassifierCheckpair still holds the contract at audit time, but the construction can no longer drift across sites.GeneratorUtils.keyElementTypeis gone; the class reverts to package-private and the classifier-tier validator no longer imports from the generators package. Review-pass nit (83bcfdf): split the cast from the value extraction invalidateChildServiceReturnTypeso theParam.Sourcedfilter usesclass::isInstance/class::castand assigns to a typed local before reading.batchKey(). Behaviour-preserving; null-tolerance contract unchanged. Tests:GraphitronSchemaBuilderTest.UnclassifiedFieldCase.CHILD_SERVICE_TABLE_BOUND_WRONG_RETURN_REJECTED(declaredLanguageRecordinstead ofList<Record>) andCHILD_SERVICE_SCALAR_WRONG_VALUE_TYPE_REJECTED(declaredMap<Record1<Integer>, Integer>for aString-valued field) pin the two rejection arms; the previously-shippedGraphQLQueryTest.films_titleUppercase_resolvesViaServiceRecordFieldDataLoadercontinues to exercise the end-to-end positive path against PostgreSQL. The dropped positive cell collapses onto the validator-and-emitter sharedRowsMethodShape.outerRowsReturnTypecall so structural drift between them is no longer reachable. Open follow-ups (deferred or tracked elsewhere): element-shape conversion when the developer’sSourcesisSet<TableRecord>/List<TableRecord>(deferred until a real schema needs it; builds on top of R61); theRow1→Record1framework switch (R61,emit-record1-keys-instead-of-row1.md);ParamSource.Context’s typed registry (tracked under `typed-context-value-registry.md). -
Auto-derive
BatchKeyfrom typedTableRecordaccessor on@recordparents (auto-derive-batchkey-from-typed-record-accessor, R60,14889c1+aabd7ea+b2ae55d): closes the@record-parent free-form-DTO rejection inFieldBuilder.classifyChildFieldOnResultTypefor the case where the parent class already exposes a typed zero-arg instance accessor returning a concrete jOOQTableRecord(single,List<X>, orSet<X>). The classifier reflects on the parent class once at build time, matches accessors by name (literal,get<Ucfirst>,is<Ucfirst>) and shape (X,List<X>,Set<X>forX extends TableRecordwhose mapped table equals the field’s@tablereturn), and produces one of two newBatchKey.RecordParentBatchKeypermits ;AccessorRowKeyedSingle(JoinStep.LiftedHop, AccessorRef)for single-cardinality fields,AccessorRowKeyedMany(JoinStep.LiftedHop, AccessorRef, Container)for list / set fields ; without requiring the schema author to add@batchKeyLifter. The three-option AUTHOR_ERROR (typed accessor /@batchKeyLifter/ typed jOOQTableRecord) replaces the previous two-option message. Model: newAccessorRef(parentBackingClass, methodName, elementClass)carries pre-resolved javapoetClassName`s, sibling of `LifterRef;BatchKey.RecordParentBatchKey’s permit list grows from 2 to 4 (still permits `RowKeyed+LifterRowKeyed); both new permits delegatetargetKeyColumns()toJoinStep.LiftedHop#targetColumns()so the DataLoader-key column tuple cannot diverge from the JOIN target columns. Builder-internal sealed hierarchy:RecordBatchKeyResolution.{Resolved, Rejected}lifts the per-field resolution into a sealed result the call site exhausts (Principle 8 "Builder-step results are sealed"); per-methodAccessorMatch.{Single, Many, CardinalityMismatch}and call-resultAccessorDerivation.{Ok, None, Ambiguous, CardinalityMismatch}capture the reflection match and reduction respectively, neither leaking pastFieldBuilder(Principle 7 "Builder-internal sealed hierarchies for multi-target classification"). The accessor-arm rewrites thejoinPathto[liftedHop]soSplitRowsMethodEmitter’s prelude reads target accessors uniformly through `JoinStep.WithTarget. Emitter:GeneratorUtils.buildRecordParentKeyExtraction’s switch grows from 2 to 4 arms; `buildAccessorRowKeySingleemitsBackingClass) env.getSource(.<accessor>()followed byDSL.row(__elt.get<Pk>(), …);buildAccessorRowKeyManyemits a typed for-loop over the accessor’sIterablereturn building aList<RowN<…>>forloader.loadMany.TypeFetcherGenerator.buildRecordBasedDataFetcherswitches the loader value type toRecord(1:1 with element-PK keys) and the dispatch toloader.loadMany(keys, Collections.nCopies(keys.size(), env))when the BatchKey isAccessorRowKeyedMany; result type still follows the field’s GraphQL cardinality.SplitRowsMethodEmitter.buildForRecordTableroutesAccessorRowKeyedManythroughbuildSingleMethod(1 record per key,scatterSingleByIdx) rather thanbuildListMethod;buildSingleMethodwidens its first-hop cast fromJoinStep.FkJointoJoinStep.WithTargetwith a conditionalwhereFilterlift, since bothFkJoin(single-cardinality SplitTableField) andLiftedHop(loadMany-many) reach it.TypeFetcherGenerator.hasSingleSplitFieldwidens to also gatescatterSingleByIdxemission on anyRecordTableFieldcarryingAccessorRowKeyedMany. The shared prelude’spkColsswitch inSplitRowsMethodEmitter.emitParentInputAndFkChainadmits all fourRecordParentBatchKeypermits viatargetKeyColumns()(lifter / accessor) andparentKeyColumns()(RowKeyed). Load-bearing keys: two new@LoadBearingClassifierCheckkeys ;accessor-rowkey-shape-resolved(the producer guarantees the parent backing class, the accessor identity, and the element class are all reflectively confirmed before emittingAccessorRowKeyedSingle/AccessorRowKeyedMany; consumed bybuildAccessorRowKeySingle/buildAccessorRowKeyManywhich castenv.getSource()and invoke the accessor without instanceof or null guards) andaccessor-rowkey-cardinality-matches-field(the producer pairsAccessorRowKeyedManywith list-cardinality fields andAccessorRowKeyedSinglewith single-cardinality; consumed bybuildRecordBasedDataFetcher’s `usesLoadMany ⇔ valueType = Recordrule).LoadBearingGuaranteeAuditTestpasses; no orphans. Test surface: unit-tierBatchKeyTest(5 cases, including a four-permit exhaustive-switch compile pin); pipeline-tierAccessorDerivedBatchKeyCase(6 cases ; list × list-accessor / list × set-accessor / single × single-accessor / ambiguous candidates / cardinality-mismatch / heterogeneous element); execution-tierAccessorDerivedBatchKeyTestruns theManyend-to-end, asserting one batched JDBC round-trip across two parents (3 element-PK keys), the(values (0, ?), (1, ?), (2, ?))shape, and per-parent record redistribution. Deviations from spec: (a)Containerenum onAccessorRowKeyedManyis preserved on the model but the for-loop iterates anyIterable, so emit no longer forks on it (acknowledged technical debt; tracked under R65 #1 follow-up); (b)Manyrows-method routes throughbuildSingleMethodrather than a new shape, sinceloadManyis 1:1 record-per-key by contract; (c)loadManyoverload requiresList<Object>of key contexts, so the dispatch passesCollections.nCopies(keys.size(), env)and the batch loader readskeyContexts[0]as before; (d)Singlepermit is fully wired through the emitter but blocked at validate-time by Invariant #10 (single-cardinalityRecordTableFieldrejection), so end-to-end execution-tier coverage waits for that gate to lift. Reviewer-pass follow-up:R65 accessor-batchkey-emitter-tighteningfiled for six architectural cleanups surfaced during the review (Containerslot,hasSingleSplitFieldpredicate union, preludedefault →arm, two-classifier reflection-walk dedup, two-site dispatch fork inbuildRecordBasedDataFetcher, unusedListAccessorOnSingleFieldfixture). Build green:mvn -f graphitron-rewrite/pom.xml install -Plocal-dbSUCCESS on Java 25; 1247 unit/pipeline tests + execution tier all pass. -
Lift
@lookupKeypartition ontoTableInputArg(dml-lookup-key-partition-on-tableinputarg, R62,b4624f4): addslookupKeyFieldsandsetFieldsprojections toArgumentRef.InputTypeArg.TableInputArg, populated once via a newTableInputArg.of(…)factory at the two construction sites (FieldBuilder.classifyArgument,MutationInputResolver.resolveInput). The narrowList<InputField.ColumnField>element type expresses the mutation-arm guarantee that DML inputs admit onlyDirect-extractedColumnField; query-side TIAs simply contribute zero entries because@lookupKeylands only on aColumnField. Three consumers drop their ad-hocSet<String>rebuild:MutationInputResolverInvariant #4 readssetFields().isEmpty();buildMutationUpdateFetcherwalkstia.setFields()(no skip-during-walk, no cast);buildMutationUpsertFetcherwalkstia.setFields()for the SET clause and reads!setFields().isEmpty()for the.doUpdate()/.doNothing()dispatch (the col/val lists still walkfields()to keep@lookupKeyfields on the insert branch).dml-mutation-shape-guaranteesconsumerreliesOnstrings updated to drop the "skip-the-set-during-walk" phrasing;LoadBearingGuaranteeAuditTestcontinues to pair the producer (FieldBuilder.buildDmlField) with the four emitter consumers. Pipeline coverage inGraphitronSchemaBuilderTest(UPDATE_TIA_PARTITIONS_FIELDS_INTO_LOOKUP_AND_SET,UPSERT_TIA_PARTITIONS_FIELDS_INTO_LOOKUP_AND_SET) asserts the typed projections land in declaration order; existing UPDATE / UPSERT execution-tier tests pass unchanged. Architectural follow-up to R22, surfaced in181c28f. -
Mutation bodies (
mutations, R22,b699c5a+d792463+4dc4c04+2e9712e+181c28f, plus pre-branch trunk history for Phase 1A / Phase 3 DELETE / Phase 6 service variants and the R50 cleanup pass): lifts all six mutation leaves out ofTypeFetcherGenerator.STUBBED_VARIANTS;MutationField.MutationInsertTableField,MutationUpdateTableField,MutationDeleteTableField,MutationUpsertTableField,MutationServiceTableField,MutationServiceRecordField. Highest-aggregate stub class going in (131 combined production rejections at the start of the work). Phase 1A (model + classifier): sharedDmlTableFieldsealed supertype permits the four DML records, all sharing(parentTypeName, name, location, returnType, tableInputArg, encodeReturn, errorChannel); oneFieldBuilder.classifyMutationInput(fieldDef, typeName)helper enforces Invariants #1 through #14 across all four DML verbs; the mutation-arm switch inclassifyMutationFieldbuilds the appropriate variant record from the resolved tia + encodeReturn. TheMutationField.DmlTableFieldlift and the four DML records actually shipped as part of R50’s cleanup pass (R50 deletedInputField.NodeIdField,NodeIdReferenceField,IdReferenceField,NodeIdInFilterFieldand folded their cross-table cases underColumnReferenceField/CompositeColumnField; the post-R50 input-field shape is what the DML emitters consume). Phase 1B (model alignment,b699c5a): replaces the broad(returnType, encodeReturn, payloadAssembly)triple onDmlTableFieldwith a single sealedDmlReturnExpression returnExpressionslot. Five arms (EncodedSingle,EncodedList,ProjectedSingle,ProjectedList,Payload) cover exactly Invariant #14’s admitted return-type set; thePayloadarm absorbs the R12-introducedOptional<PayloadAssembly>. Records went from 8 components to 6; emitters pattern-match a single sealed dispatch with noinstanceof ScalarReturnType/wrapper().isList()/Optional.orElseThrow()/payloadAssembly().isPresent()predicates. New load-bearing keydml-mutation-shape-guaranteesannotatesFieldBuilder.buildDmlField(producer) and the four DML emitters (consumers);LoadBearingGuaranteeAuditTestenforces the pairing. Phase 2 (INSERT,d792463):buildMutationInsertFetcherplus the verb-neutralbuildDmlFetcherskeleton (try/catch envelope,dslchain,payloadbind,returnSyncSuccess/catchArm) and theemitDmlReturnExpressionprojection terminator extracted from DELETE; column list and parallel values list both walktia.fields()once, values useDSL.val(in.get(name), Tables.T.COL.getDataType())for converter-mediated coercion. Execution-tiercreateFilm_insertsRowAndReturnsProjectedFilmagainst PostgreSQL verifiesRETURNING $fieldsend-to-end, resolving the verification gap DELETE shipped with. Phase 3 (DELETE): shipped pre-branch on trunk; later retrofitted to the Phase 1B shape via the samebuildDmlFetcherskeleton (buildMutationDeleteFetcherpattern-matches onf.returnExpression()viaemitDeleteEncoded/emitDeleteProjected/emitDeletePayloadhelpers, noinstanceofpredicates). Phase 4 (UPDATE,4dc4c04):buildMutationUpdateFetchershares the same skeleton; SET clause walkstia.fields()skipping@lookupKeynames, WHERE clause reusesbuildLookupWhere. Execution-tierupdateFilm_updatesRowAndReturnsProjectedFilmagainst PostgreSQL inserts a marker row, runs the mutation, asserts the SET clause wrote andRETURNING $fieldsreturned the new title withlanguageIdcarrying through unchanged. Phase 5 (UPSERT,2e9712e):buildMutationUpsertFetcheragainst the same skeleton; INSERT col/values lists walktia.fields()once (every field,@lookupKeyincluded), SET clause skips@lookupKeynames,.onConflict(<keys>)reads fromtia.fieldBindings(). Empty-SET case emits.doNothing()(jOOQ rejects.doUpdate()with no.setcalls). UPSERT additionally carries an Oracle-dialect runtime guard (jOOQ silently translatesINSERT … ON CONFLICTtoMERGE INTOwith semantics drift; jOOQ exposes no setting to disable the emulation). Two execution-tier tests cover both branches. Phase 6 (service mutations): shipped pre-branch on trunk. BothMutationServiceTableFieldandMutationServiceRecordFieldun-stubbed by delegating to the sharedbuildServiceFetcherCommonhelper; the R12 §3 try/catch wrapper, §5 Jakarta validation pre-step, and §2cresultAssemblysuccess-arm assembly all carry over for free on the mutation side. Both wear@DependsOnClassifierCheck(key = "service-catalog-strict-service-return", …). Architectural follow-ups (181c28f): a post-Phase-5 review surfaced two model lifts that don’t gate the stub-lift work but tighten the model the emitters consume; promoted to standalone roadmap items (dml-lookup-key-partition-on-tableinputarg, R62;dml-dialect-requirement-on-model, R63) with the relevant design discussion preserved there. An adjacent finding from the same review (SplitRowsMethodEmitter.unsupportedReasonreturningOptional<Rejection.Deferred>only to have callers immediately call.message()to feedbuildRuntimeStub’s `Stringparameter, dropping the typedEmitBlockReason) lives in R58’s domain rather than R22’s and is captured asruntime-stub-takes-deferred-rejection(R64). Out of scope and tracked separately: listed inputs (in: [FilmInput]), nested@tableinputs (NestingField),@nodeId-typed input fields (NodeIdDecodeKeys-extractedColumnField),ColumnReferenceField/CompositeColumnField/CompositeColumnReferenceFieldin mutation inputs (all gated as deferred at classify time), build-time INSERT column-coverage validation (deferred until jOOQ catalog reliably exposes NOT-NULL + default metadata), non-ID/non-TableBoundReturnTypereturn types on DML fields (Int/Boolean/Connection<T>rejected at classify time; anAffectedCountarm onDmlReturnExpressionis the future lift if needed),ScalarReturnType(ID)on non-@nodetables (rejected with descriptive message), transaction wrapping (caller’s responsibility viadsl), non-PostgreSQL dialects (RETURNINGandON CONFLICTare Postgres-specific; UPSERT additionally carries the Oracle runtime guard),@mutation+@servicemutual-exclusion (already rejected at classifier time). Build green:mvn -f graphitron-rewrite/pom.xml install -Plocal-dbSUCCESS on Java 25. -
Sharpen author-error messages with concrete remediations (
sharpen-author-error-messages, R59,14a5cce+7003aa2+cb20a25+ce43469): four validator rejection messages gain concrete fix suggestions instead of stopping at the diagnosis.ServiceCatalogparameter-mismatch (ServiceCatalog.java:233-254): branches on zero-arg / one-arg / many-arg cases, pre-fills the actual sole arg name, and offers two remedies (rename the Java parameter, or bind viaargMapping: "<javaParam>: <graphqlArg>"on@service); the empty-args branch suggests removing the parameter, adding a GraphQL argument, or registering a context key.MutationInputResolverlisted-@table-input rejection (MutationInputResolver.java:249-255): names the supported single-non-list@tableinput wrapper shape and points at the bulk-mutation roadmap gap.FieldBuilderpayload-multi-ctor rejection (FieldBuilder.java:1486-1492and:1600-1606): lists found ctor signatures via the sharedformatCtorSignatureshelper and suggests record conversion or removing extras.BuildContextzero-FK / multi-FK rejections (BuildContext.java:528-548): zero-FK arm explains why a single-hop key won’t resolve and offers chain-via-intermediate or condition-based alternatives; multi-FK arm pre-fills the first FK name in a{key: …}example. Re-classification:RecordTableFieldandRecordLookupTableFieldfree-form-DTO rejections inFieldBuilder.classifyChildFieldOnResultTypeflip fromRejectionKind.DEFERREDtoAUTHOR_ERRORsince R1’s@batchKeyLifteralready closes them; the[deferred]prefix was misleading authors into thinking they’re blocked on a future release. Adjacent fix: SDL source paths in the validator’s gcc-stylefile:line:col: error:lines relativise againstctx.basedir()viaGraphQLRewriteGenerator.relativiseSourceNameso the path shrinks to the natural project-relative form (e.g.src/main/resources/schema/…/sak.graphqls); falls back to the original string when the source is null, not absolute, or sits outside basedir. No structural changes to the validator pipeline; existing substring assertions still match (1219 unit-tier tests pass). -
Multi-table interface / union fetchers (
stub-interface-union-fetchers, R36,033db82+201b57b+fcb04c6+171f605+8f0a0a4+33a0670): lifts sixFieldvariants out ofNOT_IMPLEMENTED_REASONS(QueryTableInterfaceField,ChildField.TableInterfaceField,QueryInterfaceField,QueryUnionField,ChildField.InterfaceField,ChildField.UnionField) into native SQL emission across two tracks. *Track A (single-table, discriminator-column shape):QueryTableInterfaceFieldandChildField.TableInterfaceFieldemit a single SELECT against the discriminator-bearing parent table with per-participant LEFT JOINs gated ondiscriminatorColumnand per-occurrence aliases for cross-table@referenceparticipant fields (lifted asChildField.ParticipantColumnReferenceField);buildDiscriminatorFilterfires the discriminator predicates as a SQLIN (…)clause;JooqCatalog.findColumnresolves SQL column names from logical names; selection-set gating uses graphql-java 25’sType.fieldform. Track B (multi-table polymorphic, two-stage shape): newMultiTablePolymorphicEmitterproduces a stage-1 narrow UNION ALL projecting(typename, pk0..pkN, sort)across participants, then dispatches stage-2 per typename throughValuesJoinRowBuilder(the same row-builder R55 collapsed for_entities/Query.nodes/@lookupKey) with explicit per-PK-slott.<col>.eq(input.field(…))ON predicates. Composite PKs projectDSL.jsonbArray(…)assortso cursor-decode round-trips viaConnectionHelperClassGenerator.encode/decode’s existing JSONB conversion; child fields auto-discover their FK join paths via `FieldBuilder.ctx.parsePathper(parentTable, participantTable)pair. Connection mode: root and child connections sharebuildStage1ConnectionBlock, which lifts the per-branch UNION ALL into aTable<?> pagesTableso the same derived-table reference backs the page query and theConnectionResulttotalCount. Child-connection emission uses a DataLoader-batched windowed CTE (buildBatchedConnectionFetcherplusbuildBatchedConnectionRowsMethod): typedparentInput VALUESwidens toRow<N+1>for composite-PK parents (capped at 22 to fit jOOQ’s typed Row22 ceiling), per-branchJOIN parentInput ON <participant>.<fk> = parentInput.<parent_pk>emits the position-aligned composite-FK AND-chain, and aROW_NUMBER() OVER (PARTITION BY idx ORDER BY page.effectiveOrderBy())outer filter caps each parent’s rows atpage.limit(); per-parentConnectionResultshares onepagesTable. The page-rows query collapses N parents to 1 SQL statement, asserted byaddressOccupantsConnection_dataLoaderBatchesAcrossParentsandprojectItemsConnection_dataLoaderBatchesAcrossParents. Validator:validateMultiTableParticipantsrejects PK-less participants and PK-arity mismatches;validateChildConnectionParentPkrejects empty parent PK and parent-PK arity > 21 as build-timeAUTHOR_ERRORinstead of codegen-timeIllegalStateException. The earliervalidateMultiTableConnectionConstraints(the arity-1 reject from B4a) deletes once Item 1 generalises connection mode to composite-PK participants via the JSONB sort key. TypeResolver wiring:GraphitronSchemaClassGeneratoriterates non-NodeInterfaceType/UnionTypealphabetically and reads the synthetic__typenamecolumn projected by stage-1. Surface collapse (Item 3): the 5-arg and 6-argemitConnectionMethodsoverloads and the per-parent inline branch retire (B4c-1 was promoted same-day to B4c-2’s batched form); the dispatcher inemitConnectionMethodsis now a singleparentTable != nullswitch on the 7-arg signature, called directly fromTypeFetcherGenerator’s four interface / union arms. Test surface: unit-tier coverage in `TypeFetcherGeneratorTest,GraphitronSchemaClassGeneratorTest,InterfaceFieldValidationTest, andUnionFieldValidationTest(1219 total); execution-tier (233 total) covers cross-table participant fields, multi-table polymorphic root and child, connection pagination plus after-cursor plustotalCountplus inline-fragment dispatch, multi-parent DataLoader batching ratchet, composite-PK participants viapaged_a/paged_bfixture, and composite-PK parents via syntheticproject (org_id, project_id)withproject_note/project_eventchildren. Out of scope: mixed-PK-arity-or-type alignment beyond JSONB-encoded sort (PK column-name collisions across participants stay a follow-up); stage-1-as-CTE optimisation (the straight UNION ALL form is sufficient until profiling says otherwise); mixing with NodeId encoding for relay round-trip (per-field@nodeIdprojections continue in stage-2’s typed Record path);NodeIdReferenceFieldJOIN-projection form (R50 follow-up);Nodeinterface TypeResolver (already wired viaQueryNodeFetcher.registerTypeResolver). Priority number#3is embedded in emitted reason strings consumed by existing schema authors and must stay stable. Build green:mvn -f graphitron-rewrite/pom.xml install -Plocal-dbSUCCESS on Java 25; 1219 unit plus 233 execution all pass. -
Argument-level
@nodeIdarchitectural tightenings (argument-level-nodeid, R40,5064a16+9192bf7+5891293+9232887): the argument-level@nodeIdmachinery was already shipping correct user-visible behaviour out of R50; this item closes three structural seams a design review surfaced. Phase 1:Resolved.FkTargetsplits intoDirectFk/TranslatedFksub-arms. The positional-match predicate between FK target columns and NodeType key columns moves from inline checks at two call sites (FieldBuilder.classifyArgument,BuildContext.classifyInputField) into the resolver itself, which picks the variant once. Both call-site projections (argument-side and input-field-side) consume the variant; the inlinesameColumnsBySqlNamecheck deletes fromFieldBuilder.BuildContext.classifyInputFieldwas previously not running the predicate at all, silently letting the pathological FK-target shape through; the lift closes that asymmetric-gating gap. The sharedtranslatedFkRejectionReasonmethod names the R57 hint substring. Phase 2:LookupValuesJoinEmitter.addRowBuildingCorebranches the per-row decode site onCallSiteExtraction.NodeIdDecodeKeys.ThrowOnMismatchkeeps the existingGraphqlErrorExceptionfor synthesised lookup-key paths where a wrong-type id is a contract violation;SkipMismatchedElementemitscontinueand tracks aneffectiverow count, returningArrays.copyOf(rows, effective)when shrunk.LookupMapping.LookupArg.DecodedRecordretypes its decode slot fromHelperRef.DecodetoCallSiteExtraction.NodeIdDecodeKeysso the failure-mode arm rides on the model.FieldBuilder.classifyArgument’s same-table arg arm flips from `ThrowtoSkip, restoring the originally-specified Skip semantics over the first pass’s expedientThrow; the implicit scalar-IDarm (no@nodeId, NodeId-backed table) keepsThrowfor the synthesised lookup-key path. Phase 3:NodeIdArgPlanpre-resolves every@nodeId-decorated leaf reachable from a table-bound field’s argument set in one walk, threaded throughresolveTableFieldComponents → classifyArguments → classifyArgument; the three previous walks (findSameTableNodeIdUnderAsConnection,walkInputTypeForSameTableNodeId,hasSameTableNodeIdAnywhere) collapse into reads of the plan. The@asConnectionrejection, the lookup-promotion gate, and per-arg classification now share one classification pass instead of re-resolving each leaf three times. Load-bearing key:nodeid-fk.direct-fk-keys-matchannotatesNodeIdLeafResolver.resolve(producer) and three consumers (FieldBuilder.projectFilters,FieldBuilder.walkInputFieldConditions,BuildContext.classifyInputField);LoadBearingGuaranteeAuditTestpicks up the pairing automatically. Test surface: pipeline-tierInputFieldFkTargetNodeIdCase.FK_TARGET_PATHOLOGICAL_KEY_MISMATCH_DEFERRED_INPUTcovers the input-field-side asymmetric-gating closure;ArgumentSameTableNodeIdCaseextraction assertions flipped fromThrowOnMismatchtoSkipMismatchedElement. Resolver-tierNodeIdLeafResolverTestis the first resolver-tier unit test for an R6 resolver (DirectFk on matching keys, TranslatedFk on the parent_node + child_ref reproducer where the FK targetsparent_node.alt_keybut the NodeType key isparent_node.pk_id, DirectFk again on the input-field side); a newGraphitronSchemaBuilder.buildContextForTestsseam exposes the wiredBuildContextafter type classification but before field classification. Execution-tierGraphQLQueryTesttriplet (filmsByNodeIdArg_malformedIdMixedWithWellFormed_returnsWellFormedSubset,_allMalformedIds_returnsNoRows,_emptyList_returnsNoRows) covers the partial-decode skip path, the all-skipped short-circuit, and the empty-input edge. Out of scope: R57 (TranslatedFkJOIN-with-translation emission), multi-hop FK-target on the input side, mutation-key@nodeIdargs, andRecord1raw-cast template factoring. Fullmvn install -Plocal-dbclean. -
@batchKeyLifterdirective re-enables DataLoader batching on@recordparents that lack catalog FK metadata (batchkey-lifter-directive, R1,07e6954+d12e60d+4283fb2+7c284a1+b5c6749+a1a5e29): closes the twoRecordTableField/RecordLookupTableField"requires a FK join path and a typed backing class for batch key extraction" deferred rejections inFieldBuilder.classifyChildFieldOnResultTypefor free-form DTO parents (PojoResultTypeandJavaRecordTypewith non-nullfqClassName). The schema author supplies a static Java method that lifts aRowN<…>batch key out of the parent DTO; the classifier reflects on it once at build time, validates the per-position column-class match against the directive’stargetColumns, and produces aBatchKey.LifterRowKeyedcarrying aJoinStep.LiftedHop(target table + key columns, single-hop by construction) plus aLifterRef(ClassName, String)typed reference (sibling ofMethodRef, shaped after R50’sHelperRefprecedent). The emitter feeds the result into the existing column-keyed DataLoader path with no identity branching: target accessors come from a newJoinStep.WithTargetcapability mixed in byFkJoinandLiftedHop; key extraction comes from the lifter call. Surface: new directive on FIELD_DEFINITION (@batchKeyLifter(lifter: ExternalCodeReference!, targetColumns: [String!]!)); newBatchKey.LifterRowKeyedpermit (sealed hierarchy now five variants:RowKeyed,RecordKeyed,MappedRowKeyed,MappedRecordKeyed,LifterRowKeyed); newBatchKey.ParentKeyedandBatchKey.RecordParentBatchKeysealed sub-interfaces splitting the variant axis (the four catalog records exposeparentKeyColumns()renamed fromkeyColumns();LifterRowKeyedexposestargetKeyColumns()via the containedLiftedHop); the interface-levelBatchKey.keyColumns()accessor removed (a shared accessor with variant-dependent meaning violated Sealed hierarchies over enums); newJoinStep.LiftedHoppermit (sealed hierarchy now three variants:FkJoin,ConditionJoin,LiftedHop); newJoinStep.WithTargetcapability mixed in byFkJoinandLiftedHop; newBatchKeyLifterDirectiveResolverstandalone resolver, sibling to R6’s ten directive/projection resolvers, so classifier-side directive logic stays out ofFieldBuilder. Renames and narrowings:GeneratorUtils.buildRecordKeyExtraction→buildRecordParentKeyExtraction, parameter narrowed fromBatchKeytoBatchKey.RecordParentBatchKey;GeneratorUtils.buildKeyExtractionparameter narrowed fromBatchKeytoBatchKey.ParentKeyed; both narrowings turn mis-routing of the@service-only permits into compile errors rather than runtime throws.TypeFetcherGenerator.buildRecordBasedDataFetcherno longer casts toBatchKey.RowKeyed.SplitRowsMethodEmitter.emitParentInputAndFkChainreads target accessors uniformly via theJoinStep.WithTargetcapability; sealed-switch usage is reserved for the JOIN-on predicate (the genuine identity fork). The(JoinStep.FkJoin)and(BatchKey.RowKeyed)casts removed. Validator gate (Invariant #10, R1 Phase 2e):RecordTableFieldandRecordLookupTableFieldreject single-cardinality returns at validate time, promoting the previousSplitRowsMethodEmitter.unsupportedReasonruntime stub to a build-time AUTHOR_ERROR. The stub is replaced by anIllegalStateException(post-validate reachability is a classifier bug). Emitter fix (Phase 2f):SplitRowsMethodEmitter.buildListMethodWHERE-filter loop unconditionally cast everyJoinSteptoFkJoin, throwingClassCastExceptionforLiftedHoppaths. Fixed withif (!(path.get(i) instanceof JoinStep.FkJoin hop)) continue;;LiftedHopcarries no FK-side filter to apply, so the loop skips it. Load-bearing keys: per-fact@LoadBearingClassifierCheck/@DependsOnClassifierCheckpairslifter-classifies-as-record-table-fieldandlifter-batchkey-is-lifterrowkeyed(both with producer pairs onBatchKeyLifterDirectiveResolver.resolve). The single-hop invariant is a structural model property (LifterRowKeyedholds oneLiftedHop, not a list) documented in a plain javadoc comment on the rows-method prelude rather than as a keyed fact.LoadBearingGuaranteeAuditTestis unchanged. Test surface:BatchKeyLifterCasepipeline-tier coverage of the classifier matrix (POJO_PARENT_VALID_ROW1_LISTetc.) plus scalar-return rejection;MutationPayloadLifterTestexecution-tier coverage with threeCreateFilmPayloadrows (languageId[1, 2, 1]) asserting one DataLoader dispatch with two distinct keys (DataLoader key-deduplication: 3 input rows → 2 batched VALUES tuples), the SQL containslanguage_idand"language"and(values (0, ?), (1, ?)), and per-parentLanguagelists resolve correctly (English× 2,Italian× 1). Hand-rolled fixture service (noDSLContextparameter) ensures theQUERY_COUNT == 1assertion is clean ; the only JDBC round-trip is the lifter-batched language lookup. Documentation: rejection messages inFieldBuilderandServiceCatalog.dtoSourcesRejectionReasonnow reference the live directive instead of the roadmap-file path;code-generation-triggers.adocdirective table gains a@batchKeyLifterrow on the@record-parent child-fields table and the Source Map’sBatchKey/JoinSteppermit listings updated for the new variants;rewrite-design-principles.adocgains a "DTO-parent batching" subsection cross-linked from "Column value binding". Fullmvn install -Plocal-dbclean. -
Load-bearing classifier guarantee audit annotations (
load-bearing-guarantee-audit, R21,9acdf3f): codifies the "classifier rejection becomes emitter assumption" pattern named inrewrite-design-principles.adoc § "Classifier guarantees shape emitter assumptions"as a runtime-discoverable annotation pair underno.sikt.graphitron.rewrite.model.LoadBearingClassifierCheck(key, description)marks the producer arm;DependsOnClassifierCheck(key, reliesOn)(repeatable viaDependsOnClassifierChecks) marks each emitter site that relies on it.LoadBearingGuaranteeAuditTestwalkstarget/classesunder the rewrite package root, groups by key, and fails on (a) any consumer key without a matching producer, (b) any duplicate producer key, (c) blankdescription/reliesOn. A non-empty class-walk assertion guards against vacuous passes when the test is run before compile or from the wrong cwd. Producers without consumers are allowed (some checks reject for hygiene rather than because an emitter relies on them). The audit logic is exposed via package-privateaudit(Iterable<Class<?>>)returningList<AuditViolation>so a meta-test can exercise the failure-detection against a deliberate-violation fixture (auditfixture/OrphanedConsumer) without disturbing the production scan, keeping the audit’s own failure-detection durable across walker refactors. Sites annotated on landing: producersservice-catalog-strict-tablemethod-return(ServiceCatalog.reflectTableMethod) andservice-catalog-strict-service-return(ServiceCatalog.reflectServiceMethod) paired withTypeFetcherGenerator.buildQueryTableMethodFetcher/buildQueryServiceTableFetcherconsumers;column-field-requires-table-backed-parent(FieldBuilder.classifyChildFieldOnTableType) paired withTypeFetcherGenerator.generateTypeSpec’s `case ChildField.ColumnFieldarm;error-channel.mappings-constant(FieldBuilder.resolveErrorChannelpaired withErrorMappingsClassGenerator.generate); plus the consumerlesserror-type.path-message-fieldsproducer onTypeBuilder.buildErrorType. Inverse asymmetry (a new emitter that should depend on a guarantee but forgets@DependsOnClassifierCheck) is acknowledged out of scope: that drift mode falls back to the generated*Fetcherscompile failure that the principles doc already names as the safety net. Documentation:rewrite-design-principles.adoc§ "Classifier guarantees shape emitter assumptions" gains an enforcement paragraph naming the annotation pair and pointing forward at adding annotations on every new load-bearing classifier check. The annotation triple, audit test, fixture, and design-doc paragraph all landed as part of9acdf3f(R12 §2c) because R12’serror-channel.mappings-constantwas the first new live producer and the infrastructure was needed to gate it. -
EntityFetcherDispatchper-typeId VALUES emission collapsed onto a shared row-builder (entityfetcherdispatch-lookup-pipeline-collapse, R55,5aec7cd+8ac503c+aee21f6): the typedRow<N+1>array, the arity-22 cap, the per-cellDSL.val(value, table.COL.getDataType())construction, theDSL.values(rows).as(alias, "idx", "<sqlName>", …)aliasing, and the USING-args list now live in one place. NewValuesJoinRowBuilderhelper (graphitron/src/main/java/no/sikt/graphitron/rewrite/generators/util/ValuesJoinRowBuilder.java) is consumed by bothLookupValuesJoinEmitter(@lookupKeyroot and inline-child paths) andSelectMethodBody(federatedentitiesplusQuery.node/Query.nodesdispatch); the f-E SQL-shape pin (GraphQLQueryTest.nodes_perTypeIdBatch_emitsValuesJoinOrderByIdxShape) gates that thevalues/join/order bysubstring shape survives across both call sites. Helper API generic over caller slot: methods takeList<S>plusFunction<S, ColumnRef>projection plus a directive-contextString(used in arity-cap and empty-slots error messages). The lookup site keeps its richSlotrecord (argName, RootSource, decode bindings) and passesSlot::targetColumn; the dispatcher passesFunction.identity()againstList<ColumnRef>. ThecellsCodevalue-expression callback receives the caller’s slot back, so no parallel-list bridge is required. Caller-local pieces (kept off the helper): the for-loop body that fillsrows[i](lookup site does composite-key extraction and the per-rowDecodedRecordNodeId decode +GraphqlErrorExceptionon null; dispatcher readsbinding[0]/binding[1]); the idx cell expression (lookup usesDSL.inline(i), dispatcher usesDSL.val(idx, Integer.class), both render to a typedField<Integer>); any extra projections beyond the join (the dispatcher’sDSL.inline("<TypeName>").as("__typename")and the materialisedidxCol); the join syntax; and the.where(condition)/.orderBy(idxCol)chain. Reviewer-pass deltas (commit8ac503c): the original change flipped the dispatcher to.using(…)for symmetry with the lookup root path; the reviewer reverted it to.on(t.COL.eq(input.field("col", T.class)).and(…))because the dispatcher’s SELECT projection comes from<TypeName>.$fields(env.getSelectionSet(), t, env)which referencest.<col>directly, andUSINGcollapses joined columns at render time, risking interactions with$fields-emitted projections that include the joined key columns themselves. The helper still exposesusingArgsfor the lookup root path; the dispatcher’s join syntax is documented onSelectMethodBody’s class Javadoc. The `Condition condition = DSL.noCondition();declaration sits before the join body in both call sites so the SELECT chain stays symmetric and gives R36 Track B (per-typename interface filters) and any future per-arm filter a hook to AND into; jOOQ foldsnoCondition()away at render time. Other reviewer-pass deltas: dropped a transitionalValuesJoinRowBuilder.Slot(ColumnRef)record that required parallel-list bookkeeping at the lookup site; added an empty-slots guard torowTypeArgs(defensive ; both upstream classifiers already enforce non-empty key columns); arity-cap and empty-slots messages now embed a directive context ("@lookupKey"/"@key"), restoring the schema-author UX of the pre-collapse error messages;MAX_ARITYis package-private (tests are in-package, no external caller). Test surface: 16ValuesJoinRowBuilderTestcases pin arity (1, 5, 21), the 22-cap with directive context, the empty-slots guard with directive context, alias args, USING args, both idx-cell shapes (lookupDSL.inline(i), dispatcherDSL.val(idx, Integer.class)), the rich-slot callback contract, and theRow<N+1>[]/Table<Record<N+1>>convenience helpers. The f-E regression test continues to pass;FederationEntitiesDispatchTest(16 cases) all green;GraphQLQueryTest(141 cases) all green. Line deltas:LookupValuesJoinEmitter505 → 458,SelectMethodBody164 → 159; new helper 185 lines + 196 lines of unit tests. Net diff −68 lines of generator code; net code-plus-tests +313. Downstream consumer: R36 Track B stage 2 (native multi-table polymorphism, shipped atffa59e4) is the third caller ofValuesJoinRowBuilder, using the dispatcher-shape.on(…)callsite per the same<TypeName>.$fields(…)constraint. Follow-up nits (commitaee21f6): dispatcher’sColumnRefprojection switched fromc → ctoFunction.identity()(singleton, saves one lambda allocation per emit); unusedSlotlambda parameters renamed toin three places (the lookup site’s lambda still reads its slot param, so explicit naming stays there); spec design-section table for join syntax now ends with an inline pointer to "Reviewer pass deltas above" so a reader landing mid-doc isn’t misled by the original "Switch dispatcher to .using(…)" decision. -
FieldBuilderdecomposed onto the cross-cutting-concern axis (decompose-fieldbuilder, R6, Phase 1 at3f9b84c; Phases 2a/2b/2c at201c2f0+c819027+9e8fc46; Phase 5 atfd94f37; Phases 6a/6b/6c/6d/6e at38b143c+67c543d+9766982+679c560+56cd3a0; Phase 7 at42f8259; Phase 8 final mop-up at84f4be7; review-driven shape tightening atcea16e0): the parent-context-first dispatch (classifyQueryField/classifyMutationField/classifyChildField*) survives as a thin orchestrator that calls a fixed pipeline of resolvers and projects eachResolvedarm into the correctGraphitronFieldvariant. Each cross-cutting concern lifted into its own resolver returning a sealed result, sibling toArgumentRef’s sealed-variant pattern (the canonical example of Principle 7’s "builder-internal sealed hierarchies for multi-target classification" in `rewrite-design-principles.adoc). Directive resolvers (eliminate inline duplication and byte-identical rejection prose across classify arms):ServiceDirectiveResolver(@service, four-arm classify lift, sealedResolvedwithSuccess.{TableBound, Result, Scalar}/ErrorsLifted/Rejected),TableMethodDirectiveResolver(@tableMethod, two-arm lift,Resolved.{TableBound, NonTableBound, Rejected}gated byisRoot),ExternalFieldDirectiveResolver(@externalField, single-arm lift,Resolved.{Success, Rejected}),LookupKeyDirectiveResolver(@lookupKey, three-arm lift,Resolved.{Ok, Rejected}withresolveAtRoot/resolveAtChild(_, withSplitQuery)entry points). Projection resolvers (lift bundled monoliths into focused units):OrderByResolver(~230 lines, sealedResolved.{Ok, Rejected}owning the canonical@defaultOrderfallback message),LookupMappingResolver(pureprojectForLookupref-walker, total projection so noResolvedwrapper),PaginationResolver(clustersprojectPaginationSpec+isPaginationArg+resolveDefaultFirstValuesince they’re all pagination semantics),ConditionResolver(@conditionresolution, two sealed result typesArgConditionResult/FieldConditionResulteach{None, Ok, Rejected}replacing the prior dual-signal pattern),InputFieldResolver(plain-input-field classification wrappingBuildContext.classifyInputField),MutationInputResolver(DML@mutationinput classification + return-type validation +@mutation(typeName:)parsing,Resolved.{Ok, Rejected}),EnumMappingResolver(the enum-mapping axis:buildTextEnumMapping/validateEnumFilter/deriveExtraction/enrichArgExtractions/buildLookupBindings, lifted last because the helpers fan out across argument classification rather than clustering with any single earlier phase). Final mop-up (Phase 8): conflict detection (detectQueryFieldConflict,detectChildFieldConflict) stays as private helpers onFieldBuildersince the methods are trivial single-call-site logic with no isolated test surface to gain; the remaining fb-coupled callbacks (buildWrapper,parseExternalRef,parseContextArguments,liftToErrorsField,fieldArgumentNames) likewise stay onFieldBuildersince migrating them toBuildContextwould muddy that class’s schema/jOOQ-classification concern; eleven orphan imports (one model + tenBuildContextstatic imports left behind by the directive- and projection-axis lifts) removed, andfieldArgumentNames’s redundantly fully-qualified `Collectors/LinkedHashSetreferences collapsed onto the already-present imports. Review-driven shape tightening (cea16e0): five contained changes addressing dual-signal patterns and unused parameters surfaced by reviewing the lifts:LookupKeyDirectiveResolverdrops unused(BuildContext, ServiceCatalog, FieldBuilder)constructor params;OrderByResolverconsumes the classifiedArgumentRef.OrderByArgdirectly instead of looking theGraphQLArgumentup by name and re-walking the input type (drops 4 now-unreachable rejection arms + 3 orphan imports);EnumMappingResolver.validateEnumFilter’s null/fqcn/empty-string tri-state replaced with a sealed `EnumValidation.{NotEnum, Valid(fqcn), Mismatch(message)};FieldBuilder.TableFieldComponents’s six-nullable-field record replaced with a sealed `{Ok, Rejected}interface (six consumer + five producer sites updated);MutationInputResolverlifts the@mutation(typeName:)raw String into aDmlKindenum + sealedDmlKindResult.{Absent, Kind, Unknown}(replaces 8+ string-equality comparisons across three sites with exhaustive enum switches and removes an "unreachable: typeName=…" default arm). Net result:FieldBuildershrinks from 3,301 lines to ~2,534 lines (-1,007 against the counterfactual no-R6 trajectory; live size also reflects R12’s concurrent growth) and ten resolver siblings live as standalone files (~240 / 148 / 117 / 90 / 289 / 88 / 109 / 176 / 73 / 317 / 276 lines respectively), each independently testable and aligned with the sealed-result pattern thatArgumentRefset as the precedent. -
NodeId lifted out of the model (
lift-nodeid-out-of-model, R50, phases d/e1/e2-foundation/e2-rest/e3/e4a/e4b/e4c at7bf0303+2635d97+b36f230+9e38cc9+1b07b72+f2ba2c5+995bb29+a923694; phases f-A/f-B/f-C/f-D/f-E at67999cf+d43f1e3+b21a152+8fe072f+3298ac7; phases g-A/g-B/g-C at29734fb+6c54435+e4ac4ee; encoder cleanup at1275396+8a12231; status flip at0b26872; post-review notes at3459225; final retirements atfad83a7; R55 follow-on filed at72ae5cc): wire-format encoding and decoding for@nodeIdids now lives at the DataFetcher boundary; the classifier model and emitted query builders below it see decoded key tuples and standard column predicates rather thanNodeIdEncoder.hasIds(…)calls reaching across the boundary. Wire-shape variants retired:InputField.NodeIdField/NodeIdReferenceField/NodeIdInFilterField/IdReferenceField,ChildField.NodeIdField/NodeIdReferenceField,BodyParam.NodeIdIn,LookupMapping.NodeIdMapping,ArgumentRef.ScalarArg.NodeIdArg. New boundary taxonomies:CallSiteExtraction.NodeIdDecodeKeyssealed into two arms (SkipMismatchedElementfor filter call-sites where a malformed id short-circuits to "no row matches";ThrowOnMismatchfor lookup-key / mutation-key call-sites where a wrong-type id is a contract violation);CallSiteCompactionsealed root withDirect(plain projection) andNodeIdEncodeKeys(HelperRef.Encode)(encode-on-projection) arms; the third failure mode (NullOnMismatchforQuery.node/Query.nodes/ federated_entities) is dispatcher-driven rather than carrier-driven and lives inEntityFetcherDispatchClassGenerator. Composite-key column carriers: newInputField.CompositeColumnField/CompositeColumnReferenceField,ChildField.CompositeColumnField/CompositeColumnReferenceField,ArgumentRef.ScalarArg.CompositeColumnArgfor arity > 1 NodeIds; arity-1 cases stay on the existing single-column carriers (which gainextraction/compactionslots). TheComposite*variants narrow their boundary slot to the only arm the classifier produces (NodeIdDecodeKeyson input,NodeIdEncodeKeyson output) at the type system level rather than asserting via validator rule.BodyParam.ColumnPredicatesealed sub-taxonomy replaces the oldColumnEq(boolean list)shape with four predicate-arm records:Eq/In(single column) andRowEq/RowIn(composite-key tuples emittingDSL.row(c1, …, cN).eq(…)/.in(…)). Lookup arg restructure:LookupMapping.ColumnMappingretypes from a flatList<LookupColumn>toList<LookupArg>sealed intoScalarLookupArg(single-key target with optional NodeId decode) /MapInput(composite-key Map input from R5’s@lookupKey) /DecodedRecord(composite-PK NodeId where the decode runs once per row at the arg layer);InputColumnBindinggeneralises from a flat record into a sealed split (MapBinding/RecordBinding), narrowly typed per arm so the source-shape homogeneity is type-enforced rather than validator-asserted.HelperRefnew sealed sibling ofMethodRefwith separateEncode/Decodearms because the sameList<ColumnRef>plays semantically different roles on each side (call-site Java parameter list vs returnedRecordN<…>shape);GraphitronType.NodeTypegains pre-resolvedencodeMethod/decodeMethodfields read by every emitter and the encoder generator from one source of truth.NodeIdEncoderAPI: per-Node-typeencode<TypeName>(…)/decode<TypeName>(String) → RecordN<T1..TN>helpers replace the genericencode("typeId", …)/decode("typeId", …)surface;peekTypeId(String)stays as the only generic public method (used by typeId-fanout sites); the genericencode/decodeValuesbodies become private. Deleted:hasIds/hasId(query-builder helpers that did not belong in the encoder),coerceValue(the per-type decoders inlinegetDataType().convert(…)per slot statically),canonicalize(no callers;Base64.getUrlDecoderaccepts both padded and unpadded forms). Single-hop emission, two shapes: rooted-at-child (no JOIN, FK source columns are the keys) ships fully; rooted-at-parent (single-hop JOIN where FK source columns differ from target’skeyColumns) ships classifier-only withFetcherEmitterruntime stubs, deferred to R24’s expanded scope. Multi-hop FK and condition-join correlated-subquery emission stays in R24.MutationField.DmlTableField.nodeIdMetaretypes toencodeReturn: Optional<HelperRef.Encode>so the DML emitter no longer reconstructs the helper reference from a typeId string at emission time;JooqCatalog.NodeIdMetadatasurvives only as a classifier-time intermediate. Validator coverage lands one arm per new sealed variant (twoNodeIdDecodeKeys, twoCallSiteCompaction, fourColumnPredicate, threeLookupArg, twoInputColumnBinding, fiveComposite*carriers);TypeFetcherGenerator.NOT_DISPATCHED_LEAVESshrinks. Load-bearing classifier guarantees annotated at three keys (nodeid.decode.failure-mode,columnpredicate.column-arity,compaction.encode-keys) so emitter assumptions are tracked back to classifier sites. Fixture growth:nodeidfixturegains a composite-PKBarNode type for[ID!] @nodeId(typeName: "Bar")row-IN coverage and a rooted-at-parentparent_node+child_refshape (FK targets non-PK unique column) ready to drive R24’s emitter coverage. Test surface: every@nodeIdexecution test continues to round-trip (Query.node,Query.nodes, federated_entities, same-table filter, rooted-at-child reference, composite-PK lookup); SQL inspection viaExecuteListenerconfirms emitted bodies arec.eq/c.in/DSL.row(…).eq/.inover decoded key tuples rather than encodedStringids;Query.nodesper-typeId batch SQL pinned toVALUES + JOIN + ORDER BY idxshape (regression test catches dispatcher fallback to legacyWHERE row-IN); failure-mode parity verified per arm. Post-review cleanup retired two compat fallbacks (InputField.NodeIdFieldandNodeIdReferenceField’s "classified-but-inert" arms in `BuildContext) that survived the wire-shape variant deletions; both reroute toInputFieldResolution.Unresolvedwith pointedAUTHOR_ERRORreasons. Coupling: R20 (IdReferenceFieldcode generation) tombstoned and deleted in this transition (its execution-tier coverage is in R50’s pipeline test surface); R24 expanded to absorb rooted-at-parent single-hop JOIN-with-projection emission alongside its original multi-hop / condition-join scope; R55 filed as a Backlog item to collapseEntityFetcherDispatch’s bespoke per-typeId VALUES emission onto `LookupValuesJoinEmitter(the SQL shape was pinned by phase f-E but the two pipelines remain parallel). R40 (argument-level@nodeId) reduced to a small classifier-only follow-on. Inbound roadmap references inretire-synthesis-shims.md(R27) andfaceted-search.md(R13) updated to name the post-R50 column-shaped successors. Stale{@link InputField.IdReferenceField#targetTypeName}javadoc inBuildContextand "R50 phase b2b" stub messages inFetcherEmitter/TypeFetcherGeneratorretexted to point at R24. -
Consolidated test-tier guide shipped (
rewrite-test-tier-guide, R29,f621097+bb83da6): four JUnit 5 meta-annotations (@UnitTier,@PipelineTier,@CompilationTier,@ExecutionTier) added tographitron-fixturesmain scope; applied to every@Test-bearing class ingraphitron(87 classes) andgraphitron-test(8 classes);GeneratorDeterminismTestcarries@Tag("cross-cutting")directly as the sole cross-cutting test. Enforcement test added per-module: each walks its owntarget/test-classestree and fails the build if any@Test-bearing class lacks exactly one tier identity. Newgraphitron-rewrite/docs/testing.adocwith decision rubric, per-tier sections, module-location vs. tier table, and build commands. Cross-links:rewrite-design-principles.adoctier sections trimmed to one-liner pointers;docs/README.adocDetailed reference list gains the new file;.claude/web-environment.mdgains a one-line pointer. Javadoc sweep replaced "Level N" and mismatched tier prose with tier annotations acrossFieldValidationTestHelper,GraphitronSchemaBuilderTest,GeneratedSourcesSmokeTest,IdempotentWriterTest, andGeneratorDeterminismTest. UnblocksR25 rebalance-test-pyramidwhich depends on the canonical tier names. Review fixes (bb83da6): two brokenxref:links to.claude/web-environment.mdreplaced with inline-code references (file is not staged in the AsciiDoc tree); ten ` — ` em-dash occurrences replaced with semicolons or colons per the CLAUDE.md writing convention. -
graphitron-rewrite/docs/README.adoclifted into an Architecture entry point (rewrite-docs-entrypoint, R28,1b59f2e+ceb5cde): adds an eight-row module table, a six-stage end-to-end pipeline tour (RewriteSchemaLoader→GraphitronSchemaBuilder→GraphitronSchemaValidator→Generators→JavaFile.writeToPath→ consumer compile) naming thedirectives.graphqlsinjection-before-classification and orphan-sweep-after-every-emit ordering invariants thatcode-generation-triggers.adoc’s zoomed-in classification diagram leaves implicit, and a closing "Detailed reference" index. `workflow.adoc’s "Canonical path" gains a one-sentence pointer at `computed-field-with-referencein the changelog as a recent end-to-end exemplar. Per-module READMEs deliberately not maintained; the inline table is the orientation surface. Phase 1 (drop the inherited#4numbering, add a real preamble) was absorbed by R9’s AsciiDoc migration. -
ExternalCodeReference.argMappingfor Java-param binding (external-code-reference-arg-mapping, R53,4a6b731+d120892): introduces a single canonical channel for naming the GraphQL→Java parameter binding on every method-backed call:@service,@tableMethod, and every@conditionsite (field-level, argument-level, input-field-level, path-step). The schema gains anargMapping: Stringfield onExternalCodeReferencecarrying a comma-separatedjavaParam: graphqlArgmini-DSL (target-on-left, matching the internalMap<javaTarget, graphqlSource>shape and the@experimental_constructType.selectionconvention). Unmentioned parameters bind by identity; whitespace and text-block input are tolerated. The R41 per-arg@field(name:)Java-binding semantic is retired in the same change without a deprecation cycle (R41 was unshipped to consumers);@field(name:)reverts to its column-binding axis on table-backed sites and its db-string mapping onENUM_VALUE.ArgBindingMapcollapses the previousforField/identityFor*family to one axis-agnostic factoryof(Set<String>, Map<String, String>)returning sealedResult.{Ok, UnknownArgRef}; a new parserparseArgMapping(String)returns sealedParsedArgMapping.{Ok, ParseError}and enforces unique Java targets, with order-preserving iteration viaLinkedHashMap+Collections.unmodifiableMap.FieldBuilder.ExternalRefandBuildContext.ConditionDirectiveretype to carryargMappingandargMappingErrorseparately fromlookupError; failure precedence makeslookupErrorwin overargMappingErrorso "I can’t resolve the class" reads ahead of "and your argMapping has a typo." Wire-through covers all seven reflect call sites:resolveServiceField, the two@tableMethodarms (root + child),buildArgCondition,buildFieldCondition,BuildContext.resolveConditionRef, andbuildInputFieldCondition. Path-step@conditionresolves with an empty slot set, so any non-emptyargMappingrejects throughUnknownArgRef;resolveConditionRefreturns a newConditionResolution(ref, error)record so the path-step caller surfaces the parser/typo with site context ("path-step @condition: …") rather than the previous generic "could not be resolved" message. Structural-inertness rejections route through the classifier rather than the schema validator (deviation from the plan, captured as an implementation note):parseExternalRefrejectsargMappingon@externalField;TypeBuilder.buildResultTypeandbuildNonTableInputTypereject on@record; the enum-classify branch rejects on@enum. Tests across all upper tiers: newArgBindingMapTest(15 cases covering parser empty/blank, duplicate Java target, malformed entry, missing colon, text-block input, factory identity baseline, override-claims-slot, two-overrides-binding-to-same-slot, unknown GraphQL arg, path-step empty + non-empty);GraphitronSchemaBuilderTestcases for happy-path, parser-rejected duplicate, pre-reflection unknown arg, post-reflection typo guard, structural-inertness rejections on@externalField/@record/@enum, plus four cross-axis cases (ARG_CONDITION_ARGMAPPING_DUAL_BOUND,FIELD_CONDITION_ARGMAPPING,TABLE_INPUT_FIELD_CONDITION_ARGMAPPING,CONDITION_PATH_ARGMAPPING_REJECTED) that R41’s per-arg design could not express;ServiceCatalogTestpost-reflection error messages reframed from@field(name: "X")toargMapping entry 'X: Y'. Pipeline + execute coverage:filmsByServiceRenamedfixture (graphitron-test/src/main/resources/graphql/schema.graphqls) authored directly withargMapping: "filmIds: ids";GraphQLQueryTest.queryServiceTable_filmsByServiceRenamed_overrideBindsArgToDifferentlyNamedJavaParamround-trips against PostgreSQL. R41 was tombstoned under the workflow’sDiscardedterminal-state rule; R53 inherited R41’s reviewed design conclusions but ran its own Spec → Ready review cycle. -
@servicerows-method body ; first iteration (service-rows-method-body+ R49 Phase B,befc156): replaces the previously-stubbed body emitted bybuildServiceRowsMethodwith a working call site. The shared emitter now handles bothServiceTableFieldandServiceRecordField, walkingMethodRef.params()to build the developer’s call:ParamSource.Sources→ the loader’skeysparameter (passed through directly; element-shape conversion is a follow-up),ParamSource.DslContext→ adsllocal declared fromgraphitronContext(env).getDslContext(env)when needed,ParamSource.ArgandParamSource.Contextvia the existingbuildArgExtractionandgetContextArgumentpaths.ArgCallEmitter.buildMethodBackedCallArgsgains a 4-arg overload accepting asourcesExpressionCodeBlock; the legacy 3-arg overload delegates withnullso root-level@service(where Sources is rejected at classifier time per Invariants §2) still throws when it sees the variant.buildServiceRowsMethodtakes(BatchKeyField, MethodRef, ReturnTypeRef, perKeyType, parentTypeName, outputPackage); the dispatch site for both service variants threads the variant’sMethodRefthrough.FilmService.titleUppercasefixture switches fromSet<Row1<Integer>>toSet<Record1<Integer>>(classifies asBatchKey.MappedRecordKeyed, framework emitsRecord1keys viaGeneratorUtils’s `RecordKeyedbranch which usesRecord.into(Tables.FILM.FILM_ID)for extraction); body fetches each film’s title from thefilmtable and returnsMap<Record1<Integer>, String>with uppercased values. Mental-model clarification:Row1<T>is jOOQ’s SQL-expression type for tuple-IN comparisons against the database, not an application-side artifact (no value accessor);Record1<T>extendsRow1<T>and addsvalue1()for application reading. The framework’s continued emission ofRow1keys for theRowKeyed/MappedRowKeyedBatchKey variants is documented as a follow-up ;Set<TableRecord>/List<TableRecord>developer signatures still classify as those variants, so a dev choosing those shapes hits the same wall aSet<Row1<Integer>>dev would. Execution testGraphQLQueryTest.films_titleUppercase_resolvesViaServiceRecordFieldDataLoaderexercises the full path: parent SELECT followed by one batched DataLoader round-trip resolving all five films' uppercased titles. R32’s spec body collapsed to a "shipped" pointer with the open follow-ups (element-shape conversion, theRow1follow-up, strict-return-type validation againstfield.elementType(), the typed-context-value registry coordination) called out for tracking. -
ServiceRecordFieldPhase A ; DataLoader plumbing for child@servicewith scalar /@record-backed return (service-record-field,b9a6900+87a827d+f9bf585+85974ac+ Phase A close-out): liftsChildField.ServiceRecordFieldout ofTypeFetcherGenerator.NOT_IMPLEMENTED_REASONSintoIMPLEMENTED_LEAVES. Phase B (R32) fills the rows-method body; Phase A ships the variant’s classification, BatchKey carrier, DataLoader registration, lambda + key-extraction emission, and a stub rows-method that throwsUnsupportedOperationExceptionat request time. Model:ChildField.ServiceRecordFieldgains a non-nullBatchKey batchKeyfield andimplements MethodBackedField, BatchKeyField;rowsMethodName()follows the sameload<X>convention asServiceTableFieldso the existing dispatch + key-extraction infrastructure picks the variant up viaBatchKeyFieldpattern matching.elementType()accessor closes the deferral noted inset-parent-keys-on-service.mdby deriving the per-key V from the schema directly:ResultReturnTypewith non-nullfqClassName→ backing class;ScalarReturnType→ standard GraphQL scalar’s Java type (String/Boolean/Integer/Double/StringforID; custom scalars and enums fall back toStringuntil the Phase B consumer scalar registry surfaces typed Java classes); other cases fall through to the reflected outer return onMethodRef.returnType(). Builder: Site 1 (classifyChildFieldOnResultType,@record-typed parent) scalar/record-return arms becomeRejectionKind.DEFERREDwith a roadmap pointer (deriving the batch key would need lifting through the parent chain to a rooted@table, a separate design problem parallel to interface-union dispatch); Site 2 (classifyChildFieldOnTableType,@table-typed parent) lifts the BatchKey via the existingextractBatchKey(MethodRef)helper and constructs the variant. Validator:validateServiceRecordFieldrejects non-emptyjoinPathwithRejectionKind.DEFERREDuntil the lift form ships. Generator:buildServiceDataFetcherandbuildServiceRowsMethodare parameterised by(ReturnTypeRef, perKeyType)rather than the previousTableBoundReturnType+hard-codedRECORD;ServiceTableFieldpassesRECORD,ServiceRecordFieldpassesfield.elementType(). Drive-by fixes uncovered by the first child-@serviceschema fixture ingraphitron-test:dfe.getSelectionSet().getField(<name>)(non-existent API onDataFetchingFieldSelectionSet) → drop theselextraction and theselparameter from the rows-method signature (the Phase A stub throws and Phase B will reintroduce whatever shape its body needs); the loader-value-type fix that’s part ofelementType()above (the previousRECORDhard-coding meant the generator was correct forServiceTableFieldonly ;ServiceRecordFieldwould have shippedDataLoader<K, Map<K, V>>had the per-key-type lookup not been added in this Phase). Coverage: sixTypeFetcherGeneratorTestcases assert the parameterisation along the new axis (positive scalar single + list, record-backed single, mapped factory selection, mapped rows-method return shape, positional rows-method return shape); two existingserviceField_*rows-method-signature tests are updated for the droppedselparameter. Builder-tier coverage:GraphitronSchemaBuilderTest.NonTableParentCase.SERVICE_FIELD_ON_RESULT_TYPEflips from "ServiceRecordField" to "DEFERRED with @record-parent reason";ServiceFieldValidationTest.RecordCase.NO_PATHexpects no errors (variant is implemented) andWITH_LIFT_CONDITIONexpects the newjoinPathDEFERREDrejection. New fixture:FilmService.titleUppercase(Set<Row1<Integer>>, DSLContext) → Map<Row1<Integer>, String>(Phase A signature only; body throws to mirror the generated rows-method’s stub) plusFilm.titleUppercase: String @service(…)ongraphitron-test/schema.graphqls. The fixture compiles and is reachable from the schema; Phase B (R32) replaces the body and adds an end-to-end execution-tier test against PostgreSQL. Strict-return-type validation againstfield.elementType()is also Phase B’s deliverable since the structural unwrapping (Map<KeyType, V>vsList<V>) is the same logic Phase B’s body emitter encodes. -
@externalFieldresolved-reference path →ComputedFieldshipped end-to-end (computed-field-with-reference,137f9d2+8ca2c78+650de56+8a6685b): liftsChildField.ComputedFieldout ofTypeFetcherGenerator.NOT_IMPLEMENTED_REASONSintoIMPLEMENTED_LEAVES.@externalFieldgains a mandatoryreference: ExternalCodeReference!argument on the schema directive (matching@service,@tableMethod,@enum); graphql-java rejects no-arg use at parse time, so the classifier never sees a missing-arg case. NewARG_EXTERNAL_FIELD_REF = "reference"constant inBuildContext.ChildField.ComputedFieldgains a non-nullMethodRef methodfield andimplements MethodBackedField; theMethodRef.Basiccarries the captured parameterised return type (Field<X>) as a structuralTypeNameand oneParam.TypedatParamSource.Tablefor the parent table parameter. NewServiceCatalog.reflectExternalField(className, methodName, parentTableClass)mirrorsreflectTableMethodwith a stricter contract: must bepublic static, must take exactly one parameter assignable from the parent’s jOOQTable<?>class, must return parameterisedorg.jooq.Field<X>(rawFieldis rejected).FieldBuilder’s `@externalFieldarm now parses the reference, runs an alias-collision check viaJooqCatalog.findColumn(rejects when the GraphQL field name shadows a real SQL column on the parent@table), reflects the method, and constructsComputedFieldwith a populatedMethodRef; all resolution failures surface asAUTHOR_ERROR.TypeClassGenerator.emitSelectionSwitchgains aComputedFieldarm emittingcase "<name>" → fields.add(<RefClass>.<method>(table).as("<name>"));to inline the developer’s call into the projection list;build$FieldsMethodandbuildTypeSpectake a newcomputedFieldsparameter so the field actually reaches the switch (the previousflatcollection only includedColumnField/NodeIdField/TableField/LookupTableField/NestingField).FetcherEmitter.dataFetcherValuegains aComputedFieldarm emittingnew ColumnFetcher<>(DSL.field("<name>")), reading by alias from the result Record.TypeFetcherGenerator’s dispatch arm becomes a no-op (wired by `FetcherEmitter, projected byTypeClassGenerator).validateComputedFieldrejects a non-emptyjoinPath(lift form) withRejectionKind.DEFERREDuntil the@referencepath lands. Tests across all upper tiers:GraphitronSchemaBuilderTest.ComputedFieldCaseextendsSCALAR_RETURNto assert the resolvedMethodRefshape (className,methodName, singleParam.Table) plus newMETHOD_NOT_FOUND(reflection failure surfacesAUTHOR_ERRORwith the missing-method name) andNAME_COLLIDES_WITH_COLUMN(alias-collision rejection);ComputedFieldValidationTestNO_PATHflips to expect no errors (variant is now implemented),WITH_LIFT_CONDITIONexpects the newDEFERREDrejection; conflict-test fixtures atGraphitronSchemaBuilderTest:3802/3837updated withreference: {…}so the now-mandatory schema parses. New test fixture classTestExternalFieldStubprovides the reflection target for the schema-builder tests; new fixture classFilmExtensions.isEnglish(Film) → Field<Boolean>ingraphitron-fixturesprovides the execution-tier target. NewFilm.isEnglish: Boolean @externalField(reference: {className, method})field ongraphitron-test/schema.graphqls; newGraphQLQueryTest.films_isEnglish_resolvesViaExternalFieldExpressionend-to-end against PostgreSQL via-Plocal-db. Docs:code-generation-triggers.mdline 171 reflects the new directive shape and code-emission contract;graphitron-lsp.mdPhase 5 dispatch table extended with@externalFieldreference-argument completion as a tracked deliverable. The legacy no-arg form is not supported; downstream schemas (~49 known instances in Sikt projects) must addreference: { className: "…", method: "…" }when migrating to the rewrite. -
Apollo Federation 2 entity dispatch via
federation-jvm(federation-via-federation-jvm,0014be7+c964fc5+6898e78+a200e94+55a9b37+558abc7+c643ff6+09616d0+6e0904e+040434e+952a0dd+f35683b+3cb65d8+a7e71f4):Query._entities(representations: [_Any!]!): [_Entity]!now resolves natively for every type Graphitron classifies, with no per-consumer wiring beyond the existingGraphitron.buildSchema(…)call. Classify-time model: newEntityResolution(typeName, table, alternatives, nodeTypeId)sidecar onGraphitronSchema.entitiesByType, populated by a newEntityResolutionBuilderthat walks every@key-bearing or@nodetype afterTypeBuilder/FieldBuilder. Each resolution carries one or moreKeyAlternative(requiredFields, columns, resolvable, KeyShape)entries;KeyShape.NODE_IDis synthesised for everyNodeType(decoded viaNodeIdEncoder.decodeValues(typeId, id)at runtime),KeyShape.DIRECTis emitted for consumer-declared@keydirectives (rep field values map index-by-index to column values).@node+ explicit@key(fields: "id", …)dedups by promoting the consumer’s directive while pinningNODE_IDshape so the dispatcher still decodes throughNodeIdEncoderrather than treating the literal"id"string as a column value; this preserves the documentedresolvable: falseopt-out. NewFederationKeyFieldsParserrejects nested selections, dotted paths, aliases, arguments, variables, comments, and numeric values with targetedParseExceptiondiagnostics;GraphQLSelectionParseris left untouched. Build-time SDL synthesis: newKeyNodeSynthesiserregistry post-step (betweenFederationLinkApplierandTagApplierinloadAttributedRegistry) attaches@key(fields: "id", resolvable: true)to every@nodetype that does not already carry an explicit@key(fields: "id", …), so the supergraph composer sees the entity declaration. Runtime emission: newEntityFetcherDispatchClassGeneratoremits anEntityFetcherDispatchclass withfetchEntities(env)/resolveByReps(reps, env)/resolveType(env)/typenameForTypeId(typeId). Per-rep flow walks alternatives in most-specific-first order, picks the first resolvable alternative whoserequiredFieldsare all present in the rep, builds a per-rep DFE rebindingargumentsto the rep sogetTenantId(repEnv)resolves against the individual rep, decodes into a column-value row, and groups bindings by(alternative-index, tenantId)into nestedLinkedHashMap`s. Per-group dispatch issues one SELECT per group via a `VALUES (idx, col1, col2, …) JOIN <table> ORDER BY idxderived table; theidxcolumn carries through SQL soresult[row.idx] = rscatters rows back to original federation positions as a SQL property, not a Java post-processing step. Projection includesinline("Foo").as("typename")plus<TypeName>.$fields(env.getSelectionSet(), table, env); graphql-java’sDataFetchingFieldSelectionSetis type-scoped at the_entitiesDFE call site, so per-type$fieldswalks pick up only the inline fragment scoped to eachtypename(no cross-type batching needed).QueryNodeFetcher.rowsNodesandfetchByIdrewired to synthesise{typename, id}reps and callresolveByReps; the previous per-typeId loop and its canonicalize-encode-scatter round-trip disappear becauseidxcarried through SQL preserves order directly andBase64.getUrlDecoderaccepts both padded and unpadded forms. Schema wire-up replaces the placeholderfetchEntities/resolveEntityTypelambdas inGraphitronSchemaClassGenerator’s two-arg `build()withEntityFetcherDispatch::fetchEntities/EntityFetcherDispatch::resolveTypewhenentitiesByTypeis non-empty; otherwise the placeholder lambdas stay so a@link-but-no-entity schema still wraps cleanly.AppliedDirectiveEmitter.emitAstLiteralValueswitches from per-scalar enumeration toValuesResolver.valueToLiteral(…) → AstPrinter.printAst → Parser.parseValue, eliminating a class of latent custom-scalar /Float/ input-object / internally-coerced-enum bugs.ColumnRefaddscolumnClass()accessor used by the dispatcher to type the derived-tableRowarity. Federated test fixture (graphitron-test/src/main/resources/graphql/federated-schema.graphqls) is isolated from the sharedschema.graphqlsvia a secondgraphitron-mavenexecution generating intono.sikt.graphitron.generated.federated; non-federation tests keep their previous output package. Test coverage across three tiers: 11EntityResolutionBuilderTestclassify-time cases (NODE_ID synthesis with/without explicit@node(typeId:), DIRECT alternatives, multi-key, dedup, compound,resolvable: falsecarry-through, unresolvable-field demotion, nested-selection rejection, empty-fields rejection, non-@tablerejection); 19FederationKeyFieldsParserTestcases covering naked / braced / mixed whitespace / underscore-and-digit identifiers / commas / nested rejection / unbalanced braces / dotted / aliased / arguments / hash-comments / variables / numeric; 7FederationBuildSmokeTestcases (two-arg shape,_Service+_entitiesfield present,_Entityunion membership,_Service.sdlcarries synthesised@key(fields: "id")on every@nodetype, customizer invocation, one-arg → two-arg delegation); 16FederationEntitiesDispatchTestend-to-end cases against PostgreSQL (single NODE_ID rep, mixed-typename order preservation, empty representations, unknowntypename, garbage NodeId, DIRECT-shape viafilmId, type-scoped selection-set per-type projection, multi-tenancy partition issuing one SELECT per tenant, multi-alternative dispatch per rep, typename-only projection, compound key, compound partial-match yielding null, compound batching one SELECT for multiple reps, customizer-replaces-default no-SELECT-fires, most-specific tie-break selecting compound over simple, non-resolvable@keyyielding null without firing SELECT); plus 2NoFederationRegressionTestcases asserting the shared fixture builds a non-federated schema and emits only the one-argbuildSchemaoverload.getting-started.mdupdated:@linkintro broadened (a baseschema { … } @linkis also accepted), and the two-arg-form example reframed as an escape hatch for entity types Graphitron does not classify (custom fetchers must return jOOQRecord`s with a `typenamecolumn for the defaultresolveEntityTypeto recognise them). Hygiene pass shipped alongside:FEDERATION_DIRECTIVE_NAMESmoved behind an initialisation-on-demand holder so federation-jvm load failures only surface on schemas that use federation;buildRecipeErrorsmixed-error trade-off documented; the federation spec URL lifted fromFederationLinkApplier.DEFAULT_FEDERATION_SPEC_URLto a new neutralFederationSpecclass in the federation knowledge package (three callers no longer reach into a pipeline class for a constant); unusedSchemaDirectiveRegistry.FEDERATION_DIRECTIVESandisFederationdeleted (zero production callers; can be brought back if needed);federationLinkthreaded via a newAttributedRegistry(registry, federationLink)carrier returned fromloadAttributedRegistry, soKeyNodeSynthesiserandGraphitronSchemaBuilder.buildBundleno longer re-walk the registry to discover whatFederationLinkApplier.applyalready determined, andFederationLinkApplier.hasFederationLinkdeletes. Non-goals: Federation 1, customresolveEntityTypeextension point,@interfaceObject,TableInterfaceTypeas a federation entity, nested-selection@key, build-time_service.sdlartefact emission, cross-typenameSQL union batching, cross-field DataLoader sharing into the entity dispatcher. 909 unit + 23 federation tests green. -
Set<T>parent-keys on@servicemethods →MappedBatchLoader(set-parent-keys-on-service,eebf881): extends theBatchKeysealed hierarchy from two to four variants via the cross-product of container axis (Listpositional vsSetmapped) and key-shape axis (RowNvsRecordN):RowKeyed(existing),RecordKeyed(existing),MappedRowKeyed(new),MappedRecordKeyed(new).keyColumns()lifted to the sealed interface so generator switches can group by shape with multi-pattern arms without re-binding identifiers.ServiceCatalog.classifySourcesTypereplaces itsList.class-only guard with a dualisList/isSetcheck and picks the variant from the two-axis cross-product;Set<TableRecord>classifies asMappedRowKeyed(matching howList<TableRecord>classifies asRowKeyed).dtoSourcesRejectionReasonreceives the same dual check soSet<SomePlainClass>now produces "not backed by a jOOQ TableRecord" instead of falling through to the generic "unrecognized sources type" path.TypeFetcherGenerator.buildServiceDataFetcherpicksnewMappedDataLoadervsnewDataLoaderfrom the variant and types the lambda’skeysparameter asSet<KeyType>vsList<KeyType>accordingly; drive-by fix: the existing positional path was callingDataLoaderFactory.newDataLoaderWithContext(…)which does not exist on the API (the split-query path was already correct withnewDataLoader).buildServiceRowsMethodreturnsMap<KeyType, List<Record>>/Map<KeyType, Record>for mapped variants andList<List<Record>>/List<Record>for positional; the data-fetcher return type staysCompletableFuture<V>in all four cases sinceloader.load(key, env)yields a per-key promise regardless of the underlying batch-loader shape.GeneratorUtils.keyElementTypeandbuildKeyExtractiongroup by shape via multi-pattern arms. Tests:ServiceCatalogTestgains fourreflectServiceMethod_setOf*Sourcesclassification cases (TableRecord, Row1, Record1, DTO-rejection) plus alistOfRecord1regression;TypeFetcherGeneratorTestgainsserviceField_mapped*coverage of the Set/Map shapes and a regression for thenewDataLoaderfix. Unblocks production schemas that declare@servicechild fields withSet<SomeRecord>keys. -
Same-table
[ID!] @nodeIdfilter: primary-key IN predicate (3fdfbfa+19180ea): a[ID!] @nodeId(typeName: T)field on a@tableinput type whoseTresolves to the input’s own table now classifies asInputField.NodeIdInFilterFieldand emitsNodeIdEncoder.hasIds("typeId", arg, table.col1, …, table.colN), short-circuiting toDSL.noCondition()when the list is null or empty.BuildContext.classifyInputFieldadds a same-table guard beforefindUniqueFkToTable(t, t)(which would always miss for a self-FK lookup) and resolvesnodeTypeId/nodeKeyColumnsvia the same three-tier fallback asNodeIdReferenceField:JooqCatalog.nodeIdMetadatafirst, then post-first-passctx.types, then SDL-only@nodewith the catalog primary key as a last resort.BodyParammigrates from a single record to a sealed interface withColumnEq(existing scalar/IN path) andNodeIdIn(new) variants;TypeConditionsGenerator.buildConditionMethodswitches on the variant and now takesoutputPackageso it can fully-qualify the generatedNodeIdEncoderreference.walkInputFieldConditionsinFieldBuilderemitsBodyParam.NodeIdInfor the new leaf, gated bylookupBoundNamesso a future@lookupKey-bound combination still routes throughLookupMapping.NodeIdMappinginstead.ArgCallEmitter.buildNestedInputFieldExtractionnow wraps the leaf cast inList<…>whenparam.list()is true, fixing the call-site cast for list-shaped filter input fields (the spec assumed this already worked).TypeFetcherGenerator.NOT_DISPATCHED_LEAVESandGraphitronSchemaValidatorregister the new variant. Tests across three tiers:NodeIdPipelineTest.InputSameTableNodeIdCase(composite-PK, single-PK, target-not-@nodeUnresolved) using thenodeidfixturecatalog because the same-table case requires_NODE_KEY_COLUMNSmetadata that Sakila tables lack;TypeConditionsGeneratorTest(single-column, composite-column, list-of-String parameter type, mixedColumnEq+NodeIdIn);GraphQLQueryTest.films_filteredBySameTableNodeId*end-to-end against PostgreSQL, asserting both that filtered IDs return exactly those rows and that an empty list passes through tonoCondition()returning all rows.VariantCoverageTest.NO_CASE_REQUIREDcarries an entry pointing at the pipeline test, parallel to howNodeIdFieldandNodeIdReferenceFieldare already handled. Cleanup pass dropped a deadnonNullfield onBodyParam.NodeIdIn(the body always guardsarg == null || arg.isEmpty()so outer-list nullability is moot). -
Auto-emit Relay
nodes(ids:)resolver whennode(id:)exists (auto-nodes-relay-resolver,71e439f+aa33bd3+cbbc103+40e22b2+44d0201+6b865f3+4aa79f7): newQueryField.QueryNodesFieldsealed variant routed byFieldBuilder.classifyQueryFieldfor any root-query field namednodesreturning[Node]/[Node!]/[Node]!/[Node!]!;GraphitronSchemaValidatoradds a no-op arm andTypeFetcherGenerator.buildQueryNodesFetcheremits a thin delegator toQueryNodeFetcher.getNodesparallel to the existingbuildQueryNodeFetcher.QueryNodeFetcherClassGeneratorextracts the per-typeIddispatch out ofgetNodeinto a privatefetchById(env, id)helper reused by both single- and batch-paths, then adds agetNodesmethod that fansidsinto per-tenantDataLoader<String, Record>`s keyed by `getTenantId(idEnv) + "/" + path, whereidEnvis a per-idDataFetchingEnvironmentImpl.newDataFetchingEnvironment(env).arguments(Map.of("id", id)).build()so apps that vary tenant per id partition correctly (loaders share a registry across the request, so ids resolving to the same tenant batch into onehasIdsquery while ids from different tenants land in separate loaders;batchEnv.getKeyContextsList().get(0)is safe inside the batch lambda because every key in a given loader shares a tenant by construction). The batch-loader callbackrowsNodes(keys, env)groups keys bypeekTypeId, runs onedsl.select(…).from(t).where(NodeIdEncoder.hasIds(typeId, typeIds, keyCols)).fetch()per typeId, and scatters rows back to original positions via aMap<String, List<Integer>>keyed byNodeIdEncoder.canonicalize(peekTypeId(id), id)so non-canonical inputs (padded base64, the URL decoder accepts trailing=whileencode()emits the no-padding form) still match the canonical encoded id from the result row, eliminating a silent disagreement withnode(id:). NewNodeIdEncoder.canonicalize(typeId, base64Id)(decode + re-encode, null on malformed input or typeId mismatch) lives next topeekTypeId. The result-scatter projection always appends eachnodeKeycolumn to the$fieldslist (gated byif (!fields.contains(t.<col>))to dedup against$fields’s `id-driven addition; mirrorsTypeClassGenerator’s required-projection-columns pattern) and a synthetic `__typenamecolumn so the existingNodeTypeResolverstill routes by name.GraphitronContext.getTenantIdjavadoc tightened to spell out the tenant/DSLContextpartition contract: whengetDslContextvaries per id,getTenantIdMUST partition by the same key, since the loader picks oneDSLContextfromkeyContextsList().get(0)for the entire batch. Generator-side comment indispatchNodesdocuments that the registry is assumed request-scoped (the standard graphql-java pattern; cross-request reuse would let loaders and first-key contexts survive across calls and break tenant scoping). Test coverage:GraphitronSchemaBuilderTest.NODES_QUERY_FIELDclassification case,QueryNodeFieldValidationTestno-op case, and 10 execution-tierGraphQLQueryTestcases under "Query.nodes ; Relay batch dispatch" (empty / mixed-type / garbage / unknown-typeId / missing-row / padded-base64 canonicalize regression / duplicate-ids / single-tenantQUERY_COUNT == 2/ per-tenant fan-outQUERY_COUNT == 2/ id-and-other-fields-together asserting both the responseidfield and the rowsNodes encode read from the same key column). 854 unit + 154 execution tests green. -
IdReferenceFieldclassifier + synthesis shim (20b3465+afc11bc+7fc28fe+a313040+c594f0a+37f01fc+ad6303b): newInputField.IdReferenceFieldsealed variant carriestargetTypeName/fkName/qualifier/synthesizeddescribing a filter predicate that resolves to ahas<Qualifier>(s)method on the FK source’s jOOQ record class ; the shapeKjerneJooqGeneratoremits from a single FK out of the input’s resolved table.BuildContext.classifyInputFieldgains two arms between the existing scalar@nodeIdbranch and the@referencebranch: the canonical form ([ID!] @nodeId(typeName: T)with optional@reference(path: [{key:}])when the FK is ambiguous) resolves the FK viaJooqCatalog.findUniqueFkToTable(new) or the explicit@referencekey and emitsIdReferenceFieldwithsynthesized=false; the synthesis-shim arm placed before column lookup intercepts legacy@field(name: "X_ID")and bare-name forms by reverse-mapping the column name throughJooqCatalog.buildQualifierMap(new ; three lowercase keys per FK: raw qualifier, lowerCamel qualifier, plural lowerCamel qualifier; cached per source table) and synthesizesIdReferenceFieldwithsynthesized=trueplus a per-site WARN whose message namesparentTypeName.fieldNameand the canonical@nodeId(typeName:) [@reference(path: [{key:}])]replacement that future migration tooling can parse out of build logs. Shim gate iscatalog.nodeIdMetadata(targetTable).isPresent(); the same KjerneJooqGenerator-project sentinel that gates the scalarNodeIdFieldshim. New catalog helpers:findUniqueFkToTable,buildQualifierMap,qualifierForFk, plus the package-privatelocalGetQualifierreproduction ofKjerneJooqGenerator.getQualifier(UpperCamelCase fromrole + targetTable + "_id";generateRoleNamereturns"HAR"when source column equals target column, otherwise the role discriminator). Newidreffixtureschema (studieprogram + studierett, two FKs: HAR-role onstudieprogram_id, role-prefixedregistrar_studieprogramwhose qualifierRegistrarStudieprogramStudieprogramIddeliberately does not match any source column) wired throughNodeIdFixtureGenerator.METADATAso the targetstudieprogramcarries__NODE_TYPE_ID. Tests across three tiers:JooqCatalogIdRefTest(22 cases on Sakila + nodeidfixture + idreffixture forfindUniqueFkToTable/buildQualifierMap/qualifierForFk/generateRoleName),IdReferenceShimClassificationTest(5 cases ; explicit@field(name:), bare plural, bare scalar, bareid: IDfalls through toNodeIdField, role-prefixed where map key ≠ any source column),IdReferenceShimWarnFormatTest(4 cases ;parentType.fieldNameformat, FK1 + FK2 ambiguous canonical replacements both include@reference, single-FK unique replacement omits@reference), plusGraphitronSchemaBuilderTest.TableInputTypeCasecases on Sakila for canonical-form coverage and matchingNodeIdPipelineTestupdates.TypeFetcherGenerator.NOT_DISPATCHED_LEAVESregisters the new variant; code generation lifts in a follow-up tracked atroadmap/id-reference-input-field.md(Spec). 853 unit tests green. -
BatchKey.ObjectBasedremoved (batchkey-remove-objectbased): collapses theBatchKeysealed hierarchy to two variants (RowKeyed,RecordKeyed).ServiceCatalog.classifySourcesTypesplits the former singleClass<?>arm:TableRecord<?>element types now classify asRowKeyedfrom the parent table’s PK columns via a newSourcesClassificationsealed result type; non-TableRecordelement types returnDtoSourcesUnsupportedand surface asUnclassifiedFieldwith an error message naming the field, the sources parameter type, and thebatchkey-lifter-directive.mdbacklog item.GeneratorUtils.keyElementTypeandbuildKeyExtractionObjectBasedswitch arms deleted; both switches are now exhaustive over two variants.GraphitronSchemaValidator.validateServiceTableFieldObjectBasedescape hatch (hasRowOrRecordKeyedearly-return) deleted; the parent-table-PK check runs unconditionally. Test coverage:ServiceCatalogTest.tableRecordSources_classifiedAsRowKeyedanddtoSources_rejectedWithLifterDirectiveHint(classifier unit);ServiceFieldValidationTest.OBJECT_BASEDrewritten asDTO_SOURCES_REJECTEDasserting the rejection path; one pipeline case for the end-to-end DTO rejection. 747 unit tests green; fullmvn install -Plocal-dbclean. -
Interface fetchers: selection-set-aware projection (
3b982fc): replaces the unconditionaltable.asterisk()inbuildQueryTableInterfaceFieldFetcherandbuildTableInterfaceFieldFetcherwith a runtime-builtLinkedHashSet<Field<?>> fieldspopulated with the discriminator column first (always, regardless of selection set) followed byaddAll(<Participant>.$fields(env.getSelectionSet(), table, env))perParticipantRef.TableBound. The set deduplicates shared columns (e.g.titledeclared on bothFilmContentandShortContentcollapses to one reference) and preserves insertion order; the.select(new ArrayList<>(fields))substitution leaves the rest of the DSL chain (.from/.where/.orderBy/.fetch[One]) untouched. Newparticipants: List<ParticipantRef>component onQueryField.QueryTableInterfaceFieldandChildField.TableInterfaceFieldrecords, threaded byFieldBuilderfromTableInterfaceType.participants()at classification time. NewTypeFetcherGenerator.buildInterfaceFieldsListhelper isolates the field-list emission from both fetcher variants. Six newTypeFetcherGeneratorTestcases (three per fetcher:_noAsterisk_inSelectClause,_discriminatorAlwaysSelected,_participants_emitFieldsCalls); existing tests + two validation tests updated for the record constructor change. 786 unit + 144 execution tests green. Cross-table participant fields (e.g.FilmContent.ratingvia JOIN tofilm) carved out asinterface-cross-table-participant-fields.md; that follow-up will add the conditional LEFT JOIN gated onenv.getSelectionSet().contains("TypeName/fieldName")plus the fixture additions (short_descriptiononcontent,ratingonFilmContent) needed to write its execution-tier tests. -
runtime-extension-points.mdrewritten for the rewrite runtime (13bbbb3+72dda8c): replaced the legacygraphitron-commondescription with the rewrite-emitted contract. The doc now opens with the per-app interface emitted under<outputPackage>.schema.GraphitronContextbyGraphitronContextInterfaceGenerator, lists the three actual methods (getDslContext,getContextArgument,getTenantId), and shows the typed-key registration shape (b.put(GraphitronContext.class, ctx)) and the helperenv.getGraphQlContext().get(GraphitronContext.class)fromTypeFetcherGenerator.buildGraphitronContextHelper. NewgetTenantIdsection documents the previously-undocumented contract that Graphitron concatenatesgetTenantId(env) + "/" + pathto build DataLoader registry keys (perTypeFetcherGenerator.buildDataLoaderName); only the tenant prefix is pluggable, the path component is Graphitron-controlled. New "Where each concern belongs" paragraph compares jOOQConfiguration(cross-cutting),getDslContext(per-request), and schema directives (SDL business semantics), absorbing the scope of the deletedgraphitroncontext-extension-point-docs.mdBacklog item. Wiring example lifted to a pointer atgetting-started.md’s Hello World / Tenant-scoped `DSLContext/ JWT-claim-context-arguments sections. "Complementary Technologies" coverage of jOOQConfiguration,ExecuteListener, and PostgreSQL RLS preserved. "See also" no longer points atgraphitron-common/README.md. -
Bump generator-side Java floor 21 → 25 (
dec71d9): parent pom<release>21</release>→<release>25</release>plus a<requireJavaVersion>25</requireJavaVersion>enforcer rule alongside<requireMavenVersion>3.9</requireMavenVersion>;graphitron-testkeeps its<release>17</release>output ratchet (the gap it now covers is "Java-18+ syntax leak" rather than "Java-21+"). Reviewer reproduced: full reactormvn install -Plocal-dbon JDK 25 (BUILD SUCCESS, all modules green);mvn -N validateon JDK 21 fails fast atenforce-versionswith "Detected JDK … is version 21.0.10 which is not in the allowed range [25,)";graphitron-lsp.mdPhase 6 no longer owns the bump. -
@asConnectiontotalCountfield (b18b6a0+6fdd231): synthesised Connection types now carrytotalCount: Int(nullable).ConnectionResultgainstableandconditionfields populated by the connection fetcher; the existing 2-arg(result, page)convenience constructor threadsnull, nullfor the Split-Connection path, and a new 4-arg(result, page, table, condition)constructor is called bybuildQueryConnectionFetcher.ConnectionHelperClassGeneratoremits agraphitronContextshim (mirroring the per-fetcher convention) and atotalCount(DataFetchingEnvironment)static resolver that runsdsl.selectCount().from(cr.table()).where(cr.condition()).fetchOne(0, Integer.class); graphql-java invokes it only when the client selects the field, so no count SQL is emitted on queries that omittotalCount.FetcherRegistrationsEmitter.connectionBodyregisters thetotalCountcoordinate gated onconnectionType.schemaType().getFieldDefinition("totalCount"), so synthesised connections always wire it and structural connections wire it only when the SDL author declared the field; the incidentalconnectionTypeMapprojection and unusedConnectionWiringrecord were removed in the same pass.GraphitronSchemaValidator.validateConnectionTyperejects structuraltotalCountfields whose unwrapped type is notGraphQLInt, using the field’sSourceLocation(falling back to the type location for programmatic schemas) so watch-mode and IDE diagnostics highlight the exact line. Pipeline coverage:GraphitronSchemaBuilderTest.ConnectionTypeCasecasesDIRECTIVE_DRIVEN_MINIMAL(synthesised carries nullableInt),STRUCTURAL_CONNECTION(nullwhen absent),STRUCTURAL_CONNECTION_WITH_TOTALCOUNT(structural field preserved);ConnectionRegistrationsTest(synthesised registers, structural-with-Int registers, structural-without does not);ConnectionTypeValidationTest(6 cases coveringInt,Int!, absent,String,[Int!]). Execution coverage inGraphQLQueryTest: filtered count equals row-predicate count, synthesised connection count, noselect countSQL when field not selected (verified via a jOOQExecuteListenerthat records rendered statements), count SQL issued exactly once when selected. Two Backlog follow-ups filed alongside:totalCountfor nested/Split-Connection carriers (returnsnulluntil that wiring ships), and count-only execution path (skip page query when onlytotalCountis selected). -
@notGenerateddirective removed from the supported set:FieldBuilder.classifyFieldshort-circuits any application toUnclassifiedFieldwith reason "`@notGenerated` is no longer supported. Remove the directive; fields must be fully described by the schema." The check runs beforedetectChildFieldConflictso co-occurring directives don’t shadow the no-longer-supported reason. The directive definition stays indirectives.graphqlsonly so the GraphQL parser doesn’t fail withunknown directivebefore we emit our error. TheNotGeneratedFieldsealed leaf, its validator dispatch, and theNotGeneratedFieldfilters inTypeFetcherGenerator/FetcherRegistrationsEmitterare deleted. Input-field paths surface the same rejection:BuildContext.classifyInputFieldshort-circuits toInputFieldResolution.Unresolved, which propagates throughTypeBuilder.buildTableInputTypeand the nested-input recursion as anUnclassifiedTypereason;FieldBuilder.classifyArgumentpre-walks plain-input arg types and emitsArgumentRef.UnclassifiedArgso the surrounding query field becomesUnclassifiedField(necessary becauseprojectFiltersonly surfaces per-field errors when a@condition/@lookupKeygate fires, so the previously-attemptedcondErrorsentry was dead code). Silent-skip filters inTypeBuilder.buildInputTypeand the nested-input branch ofBuildContext.classifyInputFieldare removed. Tests updated:NotGeneratedFieldValidationTestdeleted;GraphitronSchemaBuilderTest.NotGeneratedFieldCasecollapsed into aNOT_GENERATED_DIRECTIVE_REJECTEDentry underUnclassifiedFieldCase;NOT_GENERATED_AND_SERVICE_CONFLICT(now subsumed by the short-circuit) deleted; newNOT_GENERATED_REJECTED_PLAIN_INPUT_ARGcase underUnclassifiedFieldCaseandNOT_GENERATED_REJECTED_TABLE_INPUT/NOT_GENERATED_REJECTED_NESTED_INPUTcases underTableInputTypeCasecover the input-field paths;notGeneratedField_isExcluded/fieldsMethod_excludesNotGeneratedFieldsdeleted (their schemas no longer build). 736 rewrite unit tests green. -
graphitron-rewrite:watchgoal (8ae55b1+6bb5419+ review-fix): newWatchMojoingraphitron-rewrite-mavenre-runs the rewrite generator on.graphqlschanges; composes with content-idempotent writes so only the files whose rendered output actually changed are written and the IDE recompiles only the touched classes. Runs the generator once on startup (skippable via-Dgraphitron.watch.skipInitial=true), resolves the watch directory set from<schemaInputs>parents, and blocks on aSchemaWatcherevent loop.SchemaWatcherwalks each root recursively at startup and registers newly-created subdirectories on the fly; theMap<WatchKey, Path> registryisConcurrentHashMapso the watch-loop thread (writes fromdispatchonENTRY_CREATE-for-directory) and the debounce thread (writes fromaddRooton re-expanded<schemaInputs>) both touch it safely. Triggers route through aDebounceExecutor(default 300 ms,-Dgraphitron.watch.debounceMs) so a burst of saves coalesces into one regeneration. Validation failures and structural errors are caught and logged with the two-arggetLog().error(msg, throwable)form on both the initial run and watch-loop catch path; the loop survives. JVM shutdown hook closes theWatchServiceand debounce executor cleanly. Tests atgraphitron-rewrite-maven/src/test/java/no/sikt/graphitron/rewrite/maven/watch/: 8SchemaWatcherTestcases (write, modify, delete, debounce coalescing, non-.graphqlsfilter, recursive subdirectory registration,OVERFLOWdispatch,addRoot-vs-dispatchregistry race) and 2DebounceExecutorTestcases (burst-coalesces-to-one,closecancels pending). Documentation: new# Watch modesubsection ingraphitron-rewrite/docs/getting-started.md. -
Service-backed and method-backed root fetchers (
c5f8497+787a8ae+8f5ef71+a0a6319+b07eec6+0730b13+7d287f5+4d85a3c+4616b67+e874b88+01b040e+5b2b87b+9eae195): closes Stubs #7.QueryTableMethodTableField,QueryServiceTableField,QueryServiceRecordFieldlift out ofTypeFetcherGenerator.NOT_IMPLEMENTED_REASONSintoIMPLEMENTED_LEAVES. NewArgCallEmitter.buildMethodBackedCallArgs(MethodRef, CodeBlock, String)walksMethodRef.params()in declaration order with per-ParamSourceemission (Argvia the existing extraction switch,ContextviagetContextArgument,DslContextas literaldsl,Tableas the suppliedTables.FOOexpression;SourcesandSourceTablethrowIllegalStateExceptionsince the classifier prevents them from reaching the emitter at root). Three newTypeFetcherGeneratorper-leaf emitters:buildQueryTableMethodFetcherdeclares a specific-table local with no cast and projects via<Type>.$fields(…), whilebuildServiceFetcherCommon(shared betweenbuildQueryServiceTableFetcherandbuildQueryServiceRecordFetcher) emits an optionaldsllocal plus a directreturn ServiceClass.method(…)with no projection (graphql-java’s column fetchers walk the records).Five classifier-time invariants enforce the strict-typed shape, all surfacing through `validateUnclassifiedField` as build-time errors. §1 and §2 share `FieldBuilder.validateRootServiceInvariants(ServiceResolution)`, called from both `classifyQueryField` and `classifyMutationField` so the mutation `@service` emitter (still in NOT_IMPLEMENTED_REASONS, lands under Stubs #4) inherits the root-shape constraints when it lifts. §1: Connection wrapper rejected on root `@service` / `@tableMethod`. §2: `ParamSource.Sources` parameter rejected at root (no parent context to batch against). §3: `@tableMethod` strict-class equality via `ClassName.equals` in `ServiceCatalog.reflectTableMethod` (rejects wider `Table<R>`); the emitter's no-cast local depends on this guarantee. §4: `DslContext` parameter supported only on `@service`. §5: strict `@service` return type via `TypeName.equals` in `ServiceCatalog.reflectServiceMethod` against `FieldBuilder.computeExpectedServiceReturnType(ReturnTypeRef)` (per-variant table covers `TableBoundReturnType` Single/List, `ResultReturnType` with non-null `fqClassName`, and skips for `ScalarReturnType` / `ResultReturnType` with null `fqClassName` / Connection-wrapped / child `@service` with non-empty `parentPkColumns`).
`MethodRef.Basic.returnType()` is now a structured javapoet `TypeName` captured once via `TypeName.get(java.lang.reflect.Type)` at reflection time. Replaces a string-FQCN field plus a `parseTypeName` round-trip in `TypeFetcherGenerator` (deleted). Comparison is structural so wildcards (`? extends X`), array depth, and multi-arg generics participate in equality faithfully; the emitter declares matching fetcher return types directly without parsing strings or widening to `Object`. `ConditionFilter` overrides `returnType()` with a static `ClassName.get("org.jooq", "Condition")`. The pre-existing duplicate `ObjectBased` branches in `ServiceCatalog.classifySourcesType` collapsed to one in passing.Test fixture: `SampleQueryService` (graphitron-rewrite-fixtures) with `popularFilms(Film, Double) -> Film` (filters via `filmTable.where(...)`; jOOQ generated tables override every `where` / `as` / `rename` overload to return the specific subtype, so filtering inside `@tableMethod` is fully compatible with §3 strict-return), `filmsByService(DSLContext, List<Integer>) -> Result<FilmRecord>`, `filmCount(DSLContext) -> Integer`. Three SDL Query fields wire them via `@tableMethod` / `@service` directives.
Coverage at every tier: 737 unit + 134 test-spec, all green. Three execution-tier cases in `GraphQLQueryTest` (filter-and-project with `QUERY_COUNT == 1`, service-table column-fetcher round-trip, service-record scalar coercion). Pipeline-tier negative cases in `GraphitronSchemaBuilderTest.UnclassifiedFieldCase` cover §1, §2, §3, §5 on both query and mutation arms. Unit-tier cases in `ServiceCatalogTest` pin the strict-validation comparison semantics (matching, mismatched raw class, mismatched inner generic, mismatched cardinality, null-expected, table-method matching/mismatched/wider/null). End-to-end `ServiceRootFetcherPipelineTest` asserts rejections surface as `ValidationError` through the full SDL → classifier → validator path.
The "load-bearing classifier guarantees → tight emitter code" pattern (compile-time failure of the generated `*Fetchers` source as the safety net for any classifier/emitter mismatch) is codified in `rewrite-design-principles.md` ("Classifier guarantees shape emitter assumptions") with both this plan's `@tableMethod` no-cast local and the pre-existing `ColumnField` requires-table-parent check as named instances. Roadmap also gained a Backlog item for exploring how to map developer-declared checked exceptions on `@service` / `@tableMethod` methods to typed GraphQL errors (`@error` types, mutation payload error unions). - `@nodeId` + `@node` directive support (`a6f5a22` + `61e4dfe` + `09cf758` + `d5e0ed4` + `f77daf7` + `f403565` + `0218054` + `19916df`): Relay Global Object Identification, end-to-end. Plan rewritten to lead with semantics ; `typeId` is a wire-format contract; `@node` requires `implements Node`; SDL wins over jOOQ metadata when both speak; PK fallback fills in omitted `keyColumns`; metadata-only synthesis fires a deprecation diagnostic at type and field sites until consumers move to declared directives. `typeId` uniqueness is validated at classify time with symmetric demotion on collision. `Query.node(id: ID!)` lands as a generated `QueryNodeFetcher` class next to the per-type `*Fetchers` ; switches on the `typeId` prefix extracted via `NodeIdEncoder.peekTypeId`, projects each branch through the existing `<TypeName>.$fields(...)` plus a synthetic `__typename` column; a registered `Node` `TypeResolver` reads `__typename` to route the row to the matching concrete `GraphQLObjectType`. Encode + decode + WHERE-builder all live on the locally-emitted `NodeIdEncoder` (final, static-only ; no override hook); `LookupValuesJoinEmitter` switched off `no.sikt.graphql.NodeIdStrategy` so the rewrite tree no longer references `graphitron-common`. `ChildField.NodeIdReferenceField` emits the FK-mirror collapse path (single-hop FK whose target columns positionally match the target NodeType's `keyColumns`) ; encodes the parent's FK source columns directly, no JOIN. The legacy reflection machinery (`PlatformIdField` records, `hasPlatformIdAccessors`, `platformIdOutputMethodNames`, `sqlToAccessorSuffix`, related tests) is fully deleted. Test fixtures replace the hand-written `platformidfixture/` catalog with output from a custom `NodeIdFixtureGenerator` (extends the upstream `org.jooq.codegen.JavaGenerator`, hard-codes `__NODE_TYPE_ID` / `__NODE_KEY_COLUMNS` for `bar` (composite key) and `baz` (single key)) so the classifier is exercised against real generator output. 706 unit tests + 14 maven tests + 128 execution tests green; six `Query.node` execution cases cover round-trip, FK-mirror reference round-trip, unknown-typeId-null, garbage-base64-null, valid-prefix-no-row-null. Federation `_entities` sharing this dispatch path is superseded by the existing "Apollo Federation via federation-jvm transform" Backlog item. Two follow-ups remain on Cleanup: retire the synthesis shim once consumer SDL migrates, and lift `NodeIdReferenceField` into a JOIN-projection form for non-FK-mirror cases. - First-class Connection / Edge / PageInfo / PlainObject / Enum variants (`0aef2c7` + `0ecde9d` + `237d6d3` + `98021043` + `476bbee1` + `9a80a1d5` + `e352b60`): six-phase pivot to "classifier is authoritative." `GraphitronType` sealed hierarchy gains `ConnectionType`, `EdgeType`, `PageInfoType`, `PlainObjectType`, and `EnumType`, each carrying its `GraphQLNamedType schemaType` populated at classification time for both directive-driven (`@asConnection` on a bare list) and structural (hand-written Connection-shaped SDL) paths. `ConnectionSynthesis` (385 lines + 243-line test) deleted; `ObjectTypeGenerator`, `EnumTypeGenerator`, `InputTypeGenerator`, and `GraphitronSchemaClassGenerator` iterate `schema.types()` exclusively, with no `assembled.getAllTypesAsList()` fallback loops and no `hasAppliedDirective("asConnection")` probes at emit time. `GraphitronSchemaBuilder.rebuildAssembledForConnections` performs a two-step rebuild: `GraphQLSchema.newSchema(existing).additionalType(...)` registers synthesised Connection/Edge/PageInfo types, then `SchemaTransformer` rewrites `@asConnection` carrier fields (bare-list return type to Connection `typeRef`, appended `first` / `after` arguments) against the updated schema, so `assembled.getType("QueryStoresConnection")` resolves and the assembled schema agrees with the model. `FieldWrapper.Connection` shrinks to `(connectionNullable, defaultPageSize)` per-site metadata; per-type metadata lives on `ConnectionType`. Phase 7 (common `schemaType()` accessor) skipped with documented rationale: five variants carry the field but consumers are specialised switches; lifting an accessor would force ~15 unrelated domain variants to carry an unused `GraphQLNamedType` for the payoff of removing ~7 `instanceof` lines. One latent bug surfaced by Phase 6's enum flip: `FieldBuilder.classifyArgument`'s loose `ctx.types.containsKey(typeName)` guard misfired on enum-typed arguments once enums entered the model; tightened to `instanceof InputType || (UnclassifiedType && GraphQLInputObjectType)`. `InputDirectiveInputTypes.NAMES` (`ErrorHandler`, `ReferencesForType`, `FieldSort`, `ExternalCodeReference`, `ReferenceElement`) skipped at classify time so they never enter `schema.types()`; `_`-prefix guard moved above the `GraphQLEnumType` branch in `TypeBuilder.classifyType`. Coverage: six `ConnectionTypeCase` classification tests, `connectionType_directVariant_emitsFieldsFromSchemaType` in `ObjectTypeGeneratorTest` (constructs `ConnectionType` / `EdgeType` records directly so an emitter bug cannot be masked by classification), `VariantCoverageTest` cases for the new variants, snapshot diffs on the test-spec `schema/` output (zero diff on Phases 4 / 6; Phase 5 expected drift from the assembled rebuild). Supersedes the `ConnectionSynthesis` entry below; the totalCount entry above builds on `connectionType.schemaType()`. - `79af12c` ; Rewrite owns `@asConnection` via emit-time synthesis: `ConnectionSynthesis.buildPlan()` scans the assembled `GraphQLSchema` for `@asConnection` on bare-list fields and produces a `Plan` (connection name to `ConnectionDef` map, `needPageInfo` flag) without touching the registry. `emitSupportingTypes()` turns the plan into sorted `TypeSpec` lists: `<ConnName>Type` and `<ConnName>EdgeType` each carry `type()` + `registerFetchers()` (bound to `ConnectionHelper`), and `PageInfoType` is synthesised when absent. `ObjectTypeGenerator.buildFieldDefinition()` rewrites directive-driven fields: replaces the bare-list return type with a `typeRef` to the synthesised Connection name and appends `first: Int = <default>` / `after: String` arguments. `GraphitronSchemaClassGenerator.generate()` wires synthesised Connection/Edge/PageInfo types into `GraphQLSchema.build()` via `.additionalType(...)`. `GraphQLRewriteGenerator.runPipeline()` emits the synthesised `TypeSpec` files to the schema sub-package. Fixture adds `stores: [Store!]! @asConnection` producing `QueryStoresConnectionType` + `QueryStoresEdgeType`; smoke test verifies both are loadable; two execution tests cover cursor pagination round-trip over the Sakila stores. Structural (hand-written) Connection types are unaffected. 122 pipeline/execution tests green; 32 new unit tests across `ConnectionSynthesisTest`, `ObjectTypeGeneratorTest`, `GraphitronSchemaClassGeneratorTest`. - Content-idempotent writes + stale-file sweep (`5c780fb` + `9526217` + `84b0af7`): `GraphQLRewriteGenerator.write()` switched from `writeTo(File)` (void, always-overwrite) to `writeToPath(Path, StandardCharsets.UTF_8)`, which skips disk writes when a SHA-256 comparison against the existing file matches (logic lives in the forked `no.sikt.graphitron.javapoet.JavaFile`). Each emitted `Path` is collected into a `Set<Path> emittedThisRun`; `sweepOrphans()` walks the six owned sub-packages non-recursively (`""` / `util` / `schema` / `types` / `conditions` / `fetchers` under `outputDirectory`), deletes any `*.java` file not in the set, and leaves everything outside those sub-packages alone. Ratchets: pipeline-tier `GeneratorDeterminismTest` in `graphitron-rewrite-test` runs the full generator against the 448-line fixture schema twice (once into two different output dirs, asserting byte-identical trees; once against the same output dir, asserting mtimes preserved); writer-tier `IdempotentWriterTest` in `graphitron-rewrite` covers tamper-detection, orphan sweep inside owned sub-packages, and scope preservation outside owned sub-packages against a trivial two-type SDL. Docs: new `## Dev loop` section in `graphitron-rewrite/docs/getting-started.md` documents the three-clause contract (determinism, minimal-change writes, clean removal) in developer-observable terms plus IntelliJ / Quarkus / Spring Boot DevTools interop. Determinism audit (grep) came back clean: zero `System.currentTimeMillis` / `Instant.now` / `UUID.randomUUID` / `System.nanoTime` in generator source, zero `hashCode`-keyed comparators, `fetcherBodies` uses `TreeMap` (stable ordering), one bare `HashMap` in `JoinPathEmitter.generateAliases` but it's a counter that is never iterated. Legacy-coexistence risk audit: rewrite-test migrated to `graphitron-rewrite-maven` during the Maven-plugin landing so no in-repo consumer has both generators active; external-consumer collision is the caller's audit to perform against their own `<outputPackage>` layout. - Self-contained rewrite aggregator build (`7df7638` + `aa0f0b7` + `7da16e7`): `mvn install -f graphitron-rewrite/pom.xml` on a clean empty local repo builds all five rewrite modules without resolving any legacy `graphitron-*` artifact. `7df7638` dropped `<module>graphitron-rewrite</module>` from the root reactor; `aa0f0b7` reparented `graphitron-rewrite-parent` off `graphitron-parent` with inlined dependencyManagement / pluginManagement / compiler (release=21) / enforcer / quick-profile blocks, and replaced `${revision}${changelist}` with hardcoded `9-SNAPSHOT` across the rewrite tree (sign-off accepted; rewrite-tree bumps are now a five-pom grep-replace). `7da16e7` forked `graphitron-javapoet` into `graphitron-rewrite/graphitron-javapoet/` under coord `no.sikt:graphitron-rewrite-javapoet` (package unchanged so rewrite-core imports are untouched; legacy copy byte-identical); swapped rewrite-main's dep; dropped a dead `graphitron-common` compile dep from `graphitron-rewrite-test` (no Java imports resolved through it) and replaced its transitive `graphql-java` path with an explicit test-scope dep on `rewrite-test`; shipped `graphitron-rewrite/scripts/verify-standalone-build.sh` that runs the aggregator against a fresh empty `mktemp -d` local repo and greps the resulting repo for forbidden coords (`graphitron-common`, `graphitron-java-codegen`, `graphitron-maven-plugin`, `graphitron-schema-transform`, legacy `graphitron-javapoet`); updated `claude-code-web-environment.md`, `rewrite-design-principles.md`, and root `README.md` to name the aggregator-local entry point. Absorbs the former Cleanup-section entry "Drop `graphitron-common` build dependency from `graphitron-rewrite`" (entry deleted from Cleanup). 695 rewrite-core unit tests green, 116 execution-tier tests green, 2 Invoker ITs green, legacy root reactor byte-identical. - Rewrite owns schema loading + directive auto-injection (`c31771d`): `RewriteSchemaLoader` at `no.sikt.graphitron.rewrite.schema` parses user schema paths via `MultiSourceReader` with auto-injection of a rewrite-local `directives.graphqls` (292-line copy of the canonical from `graphitron-common`). Filesystem-only for user sources; `SchemaParser.buildRegistry` over `MultiSourceReader` with `trackData(true)`. Switches `GraphQLRewriteGenerator` and `TestSchemaHelper` off `SchemaReadingHelper`; drops `graphitron-common` build dep from `graphitron-rewrite/graphitron-rewrite/pom.xml` (declares `graphql-java` directly). Consumer-pom fix: `graphitron-rewrite-test` dropped its `<transform>` execution (which embedded directive declarations in the assembled schema, clashing on parse with auto-injection) and pointed `<schemaFiles>` at the raw user schema. `RewriteSchemaLoaderTest` covers: two-file fixture load, `@table` auto-injection proof, missing-source error, and reader-close verification. Absorbs the Cleanup entry "Drop `graphitron-common` build dependency from `graphitron-rewrite`". - Rewrite-owned Maven plugin (`76754b3` + `8a8c5ef` + `17504dd` + review-round-2 `6026b98` + `388065b`): new `graphitron-rewrite-maven` module with `GenerateMojo` / `ValidateMojo` driven by `AbstractRewriteMojo` (5 `@Parameter` fields post-cleanup: `schemaInputs`, `outputDirectory`, `outputPackage`, `jooqPackage`, `namedReferences`), `SchemaInputExpander` (glob expansion via Plexus `DirectoryScanner`, fail-fast on zero matches, `RuntimeException`-wide catch), and `RewriteContext` defensive-copy record. `graphitron-rewrite-test/pom.xml` migrated off the legacy plugin; `enableRewrite`/`disableLegacy`/`failOnRewriteValidationError` flags removed. 14 unit tests (GenerateMojoTest, SchemaInputExpanderTest, RewriteContextTest) and 2 Maven Invoker ITs (`basic-generate` happy path, `missing-schema-inputs` fail-fast). CI-friendly parent POM antrun workaround documented in plugin pom. Review-round 2 cuts (`6026b98`): `<scalars>` / `<maxAllowedPageSize>` excised (both were silent-no-op on the config surface with zero consumers in rewrite core); `<outputDirectory>` normalised against `project.basedir` instead of CWD; `mvn graphitron-rewrite:validate` works standalone from the CLI (validate-only path substitutes an inert package sentinel so the classifier type-checks); `AbstractRewriteMojo.runGenerator` unifies the `RuntimeException` → `MojoExecutionException` wrap so both Mojos share one error envelope. Generator cleanup (`388065b`): `GraphQLRewriteGenerator` extracts `logWarnings` and `validateAndLogErrors` helpers, drops stale legacy-Mojo javadoc on the instance ctor. - Rewrite owns tagged schema inputs + description notes (`84cfd644` + `8adaaa5e` + `a937d2d1`): introduces `SchemaInput` record (sourceName + optional tag + optional descriptionNote), `SchemaInputAttribution` with fail-fast overlap check, and a `RewriteContext` record carrying `schemaInputs` + `basedir`. `TagApplier` applies `@tag(name: "<tag>")` to fields / input fields / enum values / arguments / unions (legacy parity), auto-injecting the Apollo-federation-compatible `@tag` directive declaration when the registry has none and skipping elements that already declare `@tag`. `DescriptionNoteApplier` applies a blank-line-separated note (platform-stable literal `\n\n`) to everything `TagApplier` touches plus the type declarations themselves per D2 (widened past legacy for object / interface / enum / input). `GraphQLRewriteGenerator` gains an instance `run()` entry point layering the appliers between loader and classifier; static `generate()` stays intact so the legacy Mojo keeps driving `graphitron-rewrite-test`. D2 resolved as "widen notes, keep tags narrow"; naming deviation from plan (instance method `run()` not `generate()`) because Java forbids static + instance overload on one signature ; Maven-plugin plan unifies onto one name when the static retires. Review-round 1 (`8adaaa5e`) surfaced two latent production bugs: `ObjectTypeDefinition.transform()` on an `ObjectTypeExtensionDefinition` returns a plain base definition (fixed by adding extension arms to each applier's switch calling `transformExtension(...)`), and `MultiSourceReader`'s line-terminator-based source-name tracking bleeds the last line of an unterminated input into the next source (fixed in `RewriteSchemaLoader` with a `terminated()` Reader wrapper that emits a final `\n` only when the inner stream did not). Review-round 2 (`a937d2d1`) pinned both fixes: `RewriteSchemaLoaderTest.unterminatedFirstSourceDoesNotBleedSourceNameIntoSecond` ratchet with raw-string fixture; four extension tests per applier (Interface / InputObject / Enum / Union mirroring the original Object case); and an F3 follow-up that suppresses the synthetic `\n` when the source already ends with one, so `SourceLocation.line` in parse-error diagnostics is not shifted by a synthetic trailing blank. Tests: 695 rewrite-core green (from 653 pre-landing; +31 new in the applier + pipeline suites, +9 in the review-round-2 pin, +2 in the latent-bug surfaces). - Graphitron emits a prebuilt programmatic `GraphQLSchema` (`81fa607` + `5b4ecce` -> `4088cb1` + `dabfba3` + `9b4622e`): three-commit replacement of the emitted `Graphitron.java` facade's SDL + `RuntimeWiring` assembly with a single `buildSchema(Consumer<GraphQLSchema.Builder>)` call that returns a fully wired schema. Commit A retargets `GraphitronContext` into `<outputPackage>.rewrite.schema.GraphitronContext` and switches the `graphQLContext` key from `"graphitronContext"` to `GraphitronContext.class`. Commit B lands new `<TypeName>Type` generators (enum / input / object / interface / union) in `<outputPackage>.rewrite.schema`, a `GraphitronSchema` assembler owning the shared `GraphQLCodeRegistry.Builder`, the new `Graphitron` facade, survivor-directive definitions via `additionalDirective(...)` + applications via `AppliedDirectiveEmitter` on every type / field / argument / input-field / enum-value builder, default-value round-trip via `.defaultValueProgrammatic(...)`, and a legacy-wiring bridge that keeps old emitters live during the transition. Commit C deletes `WiringClassGenerator`, `GraphitronWiringClassGenerator`, the legacy `<TypeName>Wiring` classes, and the `GraphitronWiring` aggregator; the bridge is replaced by a new `FetcherRegistrationsEmitter` that emits `codeRegistry.dataFetcher(FieldCoordinates.coordinates(type, field), value)` bodies directly into the `<TypeName>Type.registerFetchers` method; `GraphitronSchemaValidator.validateNotGeneratedField` rejects `@notGenerated` with the plan-specified error; `GeneratedSourcesLintTest.emittedSourcesDoNotImportLegacyRuntimeTypes` ratchets against FQN imports of `RuntimeWiring`, `TypeRuntimeWiring`, `SchemaGenerator`, `SchemaReadingHelper`, and upstream `no.sikt.graphql.GraphitronContext`. Three execution-tier fallout fixes landed with C: typed `(DataFetchingEnvironment env)` lambda params disambiguating the `DataFetcher` / `DataFetcherFactory` overloads on `GraphQLCodeRegistry.Builder.dataFetcher`, five `.additionalType(Scalars.GraphQLInt)`-and-friends calls in `GraphitronSchema.build` (programmatic schema doesn't auto-register built-in scalars the way `SchemaGenerator` does for SDL), and `.value(name)` alongside `.name(name)` on every enum value so graphql-java's Coercing layer doesn't reject string-matching-enum-name serializations. `graphitron-rewrite/docs/getting-started.md` ships alongside covering the five API-quality-gate cases (hello world, custom scalar, federation, tenant-scoped `DSLContext`, context arguments from a JWT claim). 649 rewrite unit tests green; 116 execution-tier tests green against the new `Graphitron.buildSchema` wiring. - `96e39df` ; Implicit column conditions for `@table` input types: `FieldBuilder.walkInputFieldConditions` carries `enclosingOverride`, `lookupBoundNames`, and a nullable `implicitBodyParams` output; every un-annotated `ColumnField` / `ColumnReferenceField` on a `TableInputArg` that is not `@lookupKey`-bound and not under an override emits a `BodyParam` with `NestedInputField` extraction, folded into the same `GeneratedConditionFilter` as column-bound scalars. `projectFilters` seeds the override flag from parent-field-level and arg-level `@condition(override:true)`; plain inputs pass `null` to keep legacy "explicit-only" semantics. `FieldBuilder.javaTypeFor` drops its `IllegalStateException` guard for `NestedInputField` now that the implicit path produces column-bound body params; `implicitBodyParam` uses `String` for `ID`-typed fields so `DSL.val` coerces at the generated call site. Pipeline: five `GraphitronSchemaBuilderTest` cases (bodyparam emitted, explicit-override-suppresses-own, explicit-suppresses-implicit, lookup-key-skipped, nested-two-level). Execution: five `GraphQLQueryTest` cases (filtersByColumn, nullField, parentFieldOverride, twoFields AND, nested two-level). `PlatformIdField` is intentionally skipped here; the now-shipped `@nodeId` + `@node` directive support replaces it with a synthesized `NodeId` and absorbs the implicit-`@nodeId` case under the same path. - Argument-resolution unification, Phase 4 (`9cf83463` + `11dc670a` + `745a2a15`): `@condition` on `INPUT_FIELD_DEFINITION`. `InputField` variants carry `Optional<ArgConditionRef> condition`; `ArgumentRef.TableInputArg` / `PlainInputArg` carry `List<InputField> fields`. `BuildContext.classifyInputField` + `readConditionDirective` host the shared classifier invoked from `TypeBuilder` (type-build time) and `FieldBuilder.classifyPlainInputFields` (per call site). `FieldBuilder.walkInputFieldConditions` walks classified fields and, via `rewrapForNested`, rebuilds each `ConditionFilter`'s `ParamSource.Arg` params against a new `CallSiteExtraction.NestedInputField(outerArgName, path)` variant; `ArgCallEmitter` emits a null-safe `instanceof Map<?, ?>` ternary chain from the top-level arg down to the leaf. Six execution tests cover single-level / override / outer-override / nested / plain / plain-outer-override shapes; `filmsOuterOverrideTableInput` and `filmsOuterOverridePlainInput` are divergence-pins against legacy's "outer owns everything" semantics. Auto-column binding for `@table` input types (63 alf call sites) spun out as its own Active plan; the enclosingOverride accumulator lands with it. Plan promoted to design doc on Done: link:../argument-resolution.md[argument-resolution.md]. - Per-type `*Wiring` classes (`cadab36` + `2c366bb`): `WiringClassGenerator` at `no.sikt.graphitron.rewrite.generators` emits one `<TypeName>Wiring` class per GraphQL type to `<outputPackage>.rewrite.wiring`, covering five categories (regular, nested with `BatchKeyField` leaves, nested without, Connection, Edge); `ConnectionWiring` / `NestedTypeWiring` are private records inside the generator and the public entry is schema-only (`generate(GraphitronSchema)`). `TypeFetcherGenerator` lost `wiring()`, `emitWiring`, `buildWiringEntry`, `buildPropertyOrRecordFetcherEntry`, `buildWiringMethod`; `GraphitronWiringClassGenerator` shrank to a pure aggregator (`.type(XxxWiring.wiring())` per class name, alphabetically sorted). Lint ratchet `GeneratedSourcesLintTest.wiringAggregatorDoesNotInlineTypeWiring` pins `GraphitronWiring.java` free of any `newTypeWiring(` call so future categories can't quietly re-inline. Follow-up `2c366bb` fixed five raw-type warnings surfaced by the refactor (threading `ParameterizedTypeName` + `WildcardTypeName` through `$T` substitution and broadening two `@SuppressWarnings`) and added two `[Backlog]` Cleanup items (PageInfo wiring decision, `TypeResolver` wiring for interface/union). - `89dfea8` ; `DSLContext` params on `@service` methods: `ServiceCatalog.reflectServiceMethod` classifies `org.jooq.DSLContext` parameters as `ParamSource.DslContext`; four `ServiceCatalogTest` cases + one `GraphitronSchemaBuilderTest` pipeline case. `reflectTableMethod` intentionally unchanged ; tracked as backlog. - `3357928` ; Sealed-switch dispatch: `TypeFetcherGenerator.generateTypeSpec` exhaustive over all `GraphitronField` leaves; stubbed leaves via `NOT_IMPLEMENTED_REASONS`. - `15f9f61e` ; Variant-coverage Phase 1: `IMPLEMENTED_LEAVES` / `NOT_DISPATCHED_LEAVES` partition invariant enforced by `GeneratorCoverageTest`. - `1e48c4ee` ; Argument-resolution Phase 1: VALUES + JOIN lookup emission for `QueryLookupTableField`. - G5 ; Inline `TableField` emission: `TypeClassGenerator.$fields` via `DSL.multiset`; seven execution tests. - `aaadb78b` ; Argument-resolution Phase 2a: inline `LookupTableField` via `InlineLookupTableFieldEmitter`; six execution tests. - `7417f53` ; Body-substring test rewrite: `TypeSpecAssertions` helper; 28 → 3 intentionally-marked body-assertion sites. - `34359b4` ; Argument-resolution Phase 2b: rows-method bodies for `SplitTableField` + `SplitLookupTableField`; exact JDBC round-trip counts asserted. - Record-fields Phase 1: `ResultType` parents; `PropertyField`, `RecordField`, `ConstructorField`, `RecordTableField` with execution tests. - Record-fields Phase 2: `RecordLookupTableField` via `deriveBatchKeyForResultType`; five execution tests. - `9ba498bc` + `7cf568f4` ; Stubbed-variant validator: `validateVariantIsImplemented` reads `NOT_IMPLEMENTED_REASONS`; build fails on rewrite validation errors by default. - `@table` + `@record` input-type fix: `@record` dominates on input types; introduces `BuildContext.warnings()` channel. - `d33ace9` ; Variant-coverage Phase 2: `ClassificationCase` interface; 26 enums retrofitted with `variants()` sets. - Java-17 output ratchet: `graphitron-rewrite-test` compile goal pinned to `release=17`. - Consolidate rewrite modules under `graphitron-rewrite/` shipped at `0e5eb86`. - `0b2e4e9` + `49d7879` ; Nesting-field emission: `ChildField.NestingField` out of stubs; eight execution tests. - `1abc31ed` + `0c449fef` + `a3afd651` ; Implicit `@reference` path inference: `BuildContext.parsePath` synthesizes single-hop `FkJoin` from the jOOQ catalog when `@reference` is absent; deletes four `SplitRowsMethodEmitter` EMPTY_PATH stub branches and the duplicate FK-count logic in `GraphitronSchemaValidator`. - `2530b93` + `f8df839` + `a063d3e` + `ef89bfb` + `1900453` ; Generated-fetcher quality pass: `ConnectionHelper.pageRequest` + emitted `PageRequest` carrier own the full pagination dance (first/last guard, backward/pageSize/cursor derivation, cursor decode, reverse ordering, selection ∪ extraFields name-dedup), with `reverseOrderBy` lifted from per-`*Fetchers`-class to one shared copy; `QueryConditionsGenerator` extracts env-aware condition orchestration into a parallel generated class so entity `*Conditions` stay pure; `$T` substitution replaces every `var`-emitting site in the generator; table-local rename from `table` → `<entity>Table` with `srcAlias` threaded through `ArgCallEmitter` + all `buildCallArgs` callers, breaking the mapper/table name collision; `FieldWrapper.DEFAULT_PAGE_SIZE` unifies four fallback sites; `seekFields: Field<?>[]` matches `decodeCursor`'s declared return type; `ConnectionResult` gains a 2-arg delegating constructor. Three emitted-source lint ratchets (`GeneratedSourcesLintTest`): no `var`, no full-package jOOQ qualification in fetcher bodies, no `graphql.*` imports in entity `*Conditions`. (xref:plans/plan-generated-fetcher-quality.adoc[plan-generated-fetcher-quality.md]) - `78e3b7c` + `1dce680` ; `SplitTableField` / `SplitLookupTableField` under `NestingField`: `GraphitronSchemaValidator.NESTED_WIREABLE_LEAVES` accepts both `BatchKeyField` variants; `TypeFetcherGenerator.generate` walks `NestingField` descendants of each `TableBackedType` root and emits a narrow `<NestedTypeName>Fetchers` class (`emitWiring=false`) for every nested type with at least one `BatchKeyField` leaf ; plain-object nesting types are absent from `schema.types()`, so the walk is a second pipeline rather than an extended filter. `GraphitronWiringClassGenerator` threads the class name via `ClassName.get(fetchersPackage, …)` so `$L::$L` emits a proper import; `GraphQLRewriteGenerator` filters `fetcherClassNames` to TypeSpecs that carry a `wiring()` method so the top-level builder doesn't invoke a missing method on nested Fetchers classes. `TypeClassGenerator.collectBatchKeyColumns` recurses into `NestingField.nestedFields()` so nested Split BatchKey columns land in the outer parent's SELECT. Coverage: `GraphitronSchemaBuilderTest` classifier case, `SplitTableFieldPipelineTest` + `NestingFieldPipelineTest` structural tests (`TypeSpecAssertions.appendsRequiredColumn` pins the outer-parent PK projection), 2 execution tests in `GraphQLQueryTest` for `Film.info.cast` and `Film.info.castByKey` each batching two parents into one round-trip. Closes the 12-count production rejection. - `86ff568` + `3246fd7` + `75e6340` ; Single-cardinality `@splitQuery` support: `FieldBuilder.deriveSplitQueryBatchKey` picks FK-column `BatchKey` for single cardinality / parent-PK `BatchKey` for list (cardinality is the direction signal); classifier rejects `@splitQuery @lookupKey` at single and multi-hop single at classifier time; `SplitRowsMethodEmitter.buildSingleMethod` emits a flat terminal-JOIN returning `List<Record>` with a `scatterSingleByIdx` scatter; `TypeClassGenerator.$fields` always appends each Split* child's BatchKey columns (deduped at runtime); `TypeFetcherGenerator` threads a null-FK short-circuit (single-cardinality fetchers extract the FK to a typed local and return `CompletableFuture.completedFuture(null)` before DataLoader dispatch); scatter-helper emission gated so `scatterByIdx` / `scatterSingleByIdx` are emitted only when the class actually uses them. `JoinStep.FkJoin` docstring corrected to describe `sourceTable` as the traversal-origin table. Coverage: 4 new `GraphitronSchemaBuilderTest` cases (positive + negative §1b / §1c), `ScatterSingleByIdxTest` (reflective unit), 3 pipeline tests in `SplitTableFieldPipelineTest`, 5 execution tests in `GraphQLQueryTest` covering shared-FK dedup (2 round-trips for 5 customers), null-FK short-circuit, non-null-FK resolution, and scatter alignment across mixed-null batches. Closes the 280-count production rejection. (xref:plans/plan-single-cardinality-split-query.adoc[plan-single-cardinality-split-query.md]) - R15 ; `f65ad06` ; Doc-drift sweep: rewrite-internal docs (`code-generation-triggers.adoc`, `rewrite-design-principles.adoc`, `argument-resolution.adoc`) realigned with `model/` taxonomy. Generators table restructured into four families (fetcher / schema / error-handling / runtime helpers); `QueryEntityField` retired with `EntityFetcherDispatch` footnote; `QueryNodesField`, `ChildField.ParticipantColumnReferenceField`, `ChildField.ErrorsField`, the five `GraphitronType` permits, the `CallSiteExtraction` two sealed sub-groupers, and the `BatchKey` two-axis enumeration all surfaced; `GraphitronSchema` schematic corrected to all five fields; `BatchKey.java` Javadoc updated to "Ten permits across two axis sub-hierarchies". Single R86 forward-ref note added for the typed-rejection / sealed-hierarchies / wire-format-boundary principles slated to consolidate into the public architecture chapter. - `3821842` + `62b51c3` + `76887cf` + `c40afb4` ; Lift `@asConnection` rejection on `@splitQuery` fields: `SplitRowsMethodEmitter.buildConnectionMethod` emits the `ROW_NUMBER() OVER (PARTITION BY fk ORDER BY …)` envelope over a `parentInput` VALUES + FK-chain aliased subquery, filtered on outer `__rn__` range, so per-parent Relay pagination works inside DataLoader batches; §2 lifts the fixed-ordering restriction by parameterizing `TypeFetcherGenerator.buildOrderByHelperMethod` on the aliased `Table` so root (`filmTable`) and Split (`a1`) call sites share one helper shape; helper-emission gate adds `SplitTableField+Connection+Argument` alongside the root-field case; classifier permanently rejects `@asConnection` + `@lookupKey` at `FieldBuilder.java:252-257` / `:266-271` (composite lookup keys disambiguate batches, but cursor pagination requires lockstep batches). `ConnectionResult` storage narrowed from `Result<Record>` to `List<Record>`. Coverage: classifier/pipeline/execution tiers all green (545 rewrite + 94 test-spec). Closes the 68-count production rejection. -
R87 (
4867dc0+d747d93+b2fcdea+0ce61c8):@servicedirectives now classify instance methods on(DSLContext)holders, restoring legacy parity. The static/instance fork lives onMethodRef.CallShape; emitter dispatches viaserviceCallTarget.MethodRefis sealed with permitsNonCondition(which permitsService/StaticOnly) andConditionFilter.reflectTableMethodcarries a positiveModifier.isStaticrejection paired withservice-catalog-tablemethod-must-be-static@LoadBearingClassifierCheck. Out-of-band:ServiceHolderFactoryextension point not added ; see runtime-extension-points.adoc. -
R5 (
0480a6bd): cleanup-and-hardening pass on the already-shipped composite-key@lookupKeypath.LookupMapping.MapInput/DecodedRecordcanonical constructors reject empty bindings; three new@LoadBearingClassifierCheckkeys (lookup-mapping-bindings-table-coherent,lookup-key-input-field-non-list,lookup-field-non-empty-args) cover the lookup pipeline with matching@DependsOnClassifierCheckconsumers onLookupValuesJoinEmitter. NewLookupMappingTestpins the type-level invariants;LookupTableFieldPipelineTestextended to assert projectedColumnMapping.MapInputshape; newCompositeKeyLookupQueryTestexecution-tier asserts the rendered SQL usesusing ("film_id", "actor_id")so single-column regressions surface in test rather than at runtime. The shipped shape isTableInputArg+MapInput, not theArgumentRef.CompositeLookupArgthe original Backlog one-liner anticipated; the unified path was preferable and is locked in by the type-level invariants. -
R38 (
97201f5…ee93207+5d82380+0839488+528fc91+50195c7): ReshapeBatchKeyintoSourceKey+ unify the rows-method seam. TenBatchKeypermits collapse toSourceKey(flat record carryingtarget,columns,path,wrap,cardinality,reader) +LoaderRegistration(container×dispatch); the rows-method seam routes through one entry point per concern:RowsMethodSkeleton.build(declaration scaffolding + body framing),RowsMethodCall.batchLoaderLambda(BatchLoader lambda),DataLoaderFetcherEmitter.build(DataFetcher dance). Threesource-key.*@LoadBearingClassifierCheckkeys (SourceRowsCall⇒Row, AccessorCall⇒Record, ServiceTableRecord target-aligned⇒empty path) paired with consumers inGeneratorUtils+SplitRowsMethodEmitter.UnifiedEmissionPinsTestpins the three-fetcher / four-rows-method routing structurally. Net type-identity count: from 10+ permits down to 1SourceKey+ 5Readersub-permits + 1LoaderRegistration. The "Sealed hierarchies over enums" worked example lifted out of the principles doc to a new sibling pagegraphitron-rewrite/docs/dispatch-axes.adoc. R75’sResultRowWalkReader permit will land as a one-permit addition on this foundation. -
R18 (
21c5e57+c699979…f228556+2a86e5e+a76383b…2a7b3ba+035ef2b+ed5ebf3+39ca34f+a672c82+bec04f8+50dbcdc+232f8e0+0bbd6f3+9f41cdc+5c9109d): Java LSP rewrite +devgoal. Replaces the Rustgraphitron-lspand the legacygraphitron-maven-plugin:introspectJSON producer with a Java LSP module undergraphitron-rewrite/graphitron-lsp, served by the singlemvn graphitron:devgoal binding127.0.0.1:8487. Phases 0–6 delivered: lsp4j scaffold (Phase 0),dev-goal binding + watchers + UTF-8↔UTF-16 position conversion (Phase 1), in-processGraphQLRewriteGenerator.buildCatalog()returning tables / columns / FKs / scalars (Phase 2), per-directive completion + diagnostics + Markdown hover for@field/@reference(Phase 3), goto-definition into the jOOQ-generated source tree (Phase 4),@service/@condition/@recordautocomplete + hover + diagnostics off a JDK 25java.lang.classfile-driven scan including the Phase 5d directive-shape correction (descend through the outerservice:/condition:/record:arg into the nestedExternalCodeReference) and Phase 5e multi-module reactor visibility (MavenSession.getAllProjects()→ every reactor’s compile-output directory;RewriteContext.classpathRootsfield + six-arg back-compat overload), and-parameters-missing detection viaParameter.name == null(Phase 5a–5e), and the bonede→jtreesitter binding swap with vendoredtree-sitter-graphqlgrammar source plus per-platform native build (Phase 6). Phase 7 (Rust archival,IntrospectMojodeletion, consumer migration docs) carved out into R91; Javadoc surfacing + per-line definitions +@externalFieldwalk +argMappingautocomplete deferred into R90; multi-platform native CI tracked under R89. 90+ LSP + 48 graphitron-maven module tests green; full reactor build green includinggraphitron-sakila-exampleagainst the new instance-@servicefixture from R87. -
R134 (
36122dc+7fadbda): Fix mutation empty-input short-circuit to usenewRecordfor single-record payloads.TypeFetcherGenerator.buildMutationDmlRecordFetcherbranches the empty-list arm ondataIsList:DSL.using(…).newResult(<pkProjection>)for the projected-list arm,DSL.using(…).newRecord(<pkProjection>)for the single-record arm (mutations whose direct return is a single payload, e.g.opprettX(input: [XInput]): XPayload!). The non-empty branch was already gated ondataIsListviafetch()/fetchOne(); this aligns the empty arm. Regression coverage is owned by the compilation tier:graphitron-sakila-example/schema.graphqlsdeclarescreateFilmsPayload(in: [FilmCreateInput!]!): FilmPayload @mutation(typeName: INSERT), the exact bulk-input + single-payload shape that triggered the bug. The generatedMutationFetchers.createFilmsPayloademitsRecord1<Integer> payload = DSL.using(dsl.configuration()).newRecord(Tables.FILM.FILM_ID)and is compiled against real jOOQ classes; a regression tonewResult(…)would re-emitResult<Record>into aRecord1<Integer>local and fail compilation. Scoped to INSERT because bulk UPDATE/UPSERT onMutationDmlRecordFieldstill throw upstream. Follow-up worth filing separately: the non-empty branch on bulk-input + single-record-payload calls.fetchOne()against multi-rowvaluesOfRows(…)VALUES, discarding N-1 returned keys at runtime; the compile bug is fixed but the runtime coherence question (validator-side rejection vs. emit lift to.fetch()) deserves its own item. -
R238 (
9451dff…34ebddd+ reworkd4824e1): ServiceMethodCall walker carrier across the four root sync@servicepermits (QueryServiceTableField,QueryServiceRecordField,MutationServiceTableField,MutationServiceRecordField). Each permit dropsMethodRef method/MethodBackedFieldand gains aServiceMethodCall serviceMethodCallslot via the newServiceFieldsibling interface;buildServiceFetcherCommondrives generation throughServiceMethodCallEmitter. Lands the walker-carrier plumbing every subsequent slice inherits:WalkerResult<C>sealed wrapper, theServiceMethodCallErrorsub-seal ofAuthorError, graphitron-sideDiagnostic/Severitywith the LSP projector at the wire boundary, and the orchestrator’s collect-Err-exclude-field flow (ValidationReport.walkerDiagnostics).ContextArgumentClassifiergrows aServiceFieldharvest arm;ConflictSite.sitewidens to a two-arm sealed identifier. Shipped as a translator over a resolvedMethodRef.Servicerather than fresh SDL+classloader reflection; the substrate absorption, multi-arg ctors, silent-first-match retirement, and the per-arm typed-error taxonomy (10 arms trimmed to the 2 the translator produces,MultipleDslContextSlots+ParameterUnbindable) are carved into R256 (service-walker-substrate-absorption). Full pipeline green end-to-end. -
R243 (
16c85fd+6fd3878): Per-field direction in@order/@defaultOrderviaFieldSort.direction. Lifts the whole-specOrderBySpec.Fixed.direction: Stringdown onto a per-entry typedColumnOrderEntry.direction: SortDirection(nested enum carryingjooqMethodName()+flipped()), so a single fixed spec expresses heterogeneous order (rental_rate DESC, title ASC).OrderByResolverreads the directive-leveldirection:default (ASC when absent on@defaultOrder, ASC for@order), pushes it down per-entry with per-fieldFieldSort.direction:winning, and precomputesFixed.uniformAsconce; PK-fallback andindex:/primaryKey:synthesised entries stampSortDirection.ASCexplicitly (fork b). Emission at every fixed-spec call site (TypeFetcherGenerator,InlineTableFieldEmitter,SplitRowsMethodEmitter) switchesfixed.jooqMethodName()→col.direction().jooqMethodName(). The@orderByhelper bodies dispatch onuniformAsc: uniform-ASC keeps the runtimedir-flips behaviour, mixed is direction-locked (per-entry directions emitted verbatim, runtimedirignored) ; the opt-out semantics over multiplier, per Stability through simplicity + Generation-thinking. Schema change is purely additive (direction: SortDirectiononFieldSort, no default).flipped()retained on the enum per settled note 3 (single-place ASC↔DESC algebra; pinned by the unit test, unused by the build-time emitter). Coverage: unitOrderBySpecSortDirectionTest; pipelineGraphitronSchemaBuilderTest(PER_FIELD_DIRECTION_DEFAULT_ORDER,DIRECTIVE_LEVEL_DIRECTION_PUSHES_DOWN,PER_FIELD_DIRECTION_ORDER_ENUM_VALUE,NO_DEFAULT_ORDER_PK_FALLBACK,DEFAULT_ORDER_DIRECTION_DESC); executionfilmsByRateDescTitleAsc_executesHeterogeneousOrder+filmsOrderedConnection_mixedOrderEnumValue_ignoresRuntimeDirection(both ordering onrental_rate DESC, title ASCso the DESC primary and ASC secondary-tiebreak are independently observed).6fd3878switched the demonstrators off the seed-uniformrelease_yearontorental_rateso the primary DESC is no longer a no-op tie. Full reactor green. -
R258 (
ac1eee4): Payload-returning UPDATE onto theUpdateRowscarrier. The payload-return UPDATE shapes (updateFilmPayload/updateFilmsPayload) now classify as the newMutationField.MutationUpdatePayloadField/MutationBulkUpdatePayloadFieldleaves (bothUpdateRowsField, non-OptionalInputArgRef+UpdateRowsslots, noDmlKinddiscriminator) throughFieldBuilder.classifyUpdatePayloadField→UpdateRowsWalker(PK-or-UK matched-key membership), neverMutationInputResolver.resolveInput’s `@valuepartition. With R246’s direct-return path, no UPDATE path reads@value; the precondition for R188 retiring the directive. The shared record-carrier leaves shrink their liveDmlKindrange by rejecting UPDATE in their compact constructors (MutationDmlRecordField→ {INSERT, UPSERT, DELETE},MutationBulkDmlRecordField→ {INSERT, DELETE});MutationInputResolver’s UPDATE `@valueblock becomes a loudIllegalStateException(classifier guarantee made loud). Emit:TypeFetcherGeneratorextracts sharedbuildSingleRecordTwoStepFetcher/buildBulkRecordTwoStepFetcherskeletons parameterized on a carrier-source chain seam (chainFn/perRowBodyFn, not a re-switchedDmlKind);buildCarrierUpdateChainSingle/buildCarrierBulkPerRowUpdateBodysource SET/WHERE fromsetGroupsOf(updateRows().setColumns())/keyGroupsOf(updateRows().keyColumns()), nevertia.setFields(). The payload data field’sSingleRecordTableFieldclassification is grounded by the SDL-coordinate-keyedDmlEmittedbinding (RecordBindingResolver.groundDmlMutationField+ theclassifyChildFieldOnResultTypeunified pass), so no per-fieldreclassifyis needed on the non-DELETE path.FilmUpdateInputdrops@value(shared across all four UPDATE mutations). Coverage:SingleRecordPayloadPipelineTestUPDATE arm split onto the new leaves (@valuedropped from the UPDATEinputBody, UPSERT kept for R188);GraphitronSchemaBuilderTesttruth-table +@condition/UpdateRowsErrorpayload-rejection +DmlRecordprojection cases; execution round-tripsupdateFilmPayload_updatesRowAndReturnsPayloadWithSingleDataField+bulkUpdateWithThreeRowsInNonPkOrderPreservesInputOrderInResponsegreen after the@valuedrop. Full reactor green. Unblocks R188. -
R265 (
8586cbd+53d3da2): Fix non-compilingnew GraphqlErrorException(String)in the NodeIdThrowOnMismatchdecode helpers. Both throw arms inCompositeDecodeHelperRegistry.buildHelper(scalar:131, reachable; list:113, defensive/unreachable today) switch from the non-existent String constructor to the builder formGraphqlErrorException.newErrorException().message(MISMATCH_MESSAGE).build(), mirroring the four already-correct sites (LookupValuesJoinEmitter,TypeFetcherGenerator×2,InputBeanInstantiationEmitter). The bug rode in via R260’s lift of NodeId decode out ofArgCallEmitterinto the registry; it survived because the existingCompositeDecodeHelperRegistryTeststring assertions pin only the FQNgraphql.GraphqlErrorException, which renders identically in broken and builder form, and no fixture drives aThrowOnMismatcharm throughjavac. The compilation-tier regression guard (Deliverable 2) was deferred to R273: the scalar arm is reachable only via the legacy_NODE*metadata path (FieldBuilder’s bare-`IDblock), so a compile fixture cannot be built without exercising legacy behavior R273 is retiring.FetcherEmitter:284verified as a correctUnsupportedOperationExceptionsite, not a sixthGraphqlErrorExceptionsite. No new fixture or assertion; the existing unit pins stay green against the builder form. -
R244 (
622a470+0db48be+999b86e+845fc25+95ae80a+ rework18d46d4): Error-channel slice 1, the typedOutcome<T>transport for root@serviceoutcome fields. Retires the developer-payloadconstructionpath (ErrorChannel.PayloadClass, thePayloadConstructionShapefamily,payloadFactory*/declareEarlyPayload*,ErrorRouter.dispatch) for the four root@servicevariants, replacing it with a request-timeOutcomesource the fetcher returns:Success(value)on the happy path,ErrorList(errors)on the mapped-error path, with the unmapped fallback stillErrorRouter.redact. The wrapper resolves the localContext draft’s silent errors-drop by construction (non-null source, so graphql-java always descends into the outcome type’s children). New pieces: the generatedOutcomeruntime type (OutcomeClassGenerator), theOutcomeTypeclassification,ErrorChannel.Mapped, the output-walkingErrorChannelWalker(R238’s analogue, absorbing the channel-rule + accessor-coverage checks),ChannelCatchArmEmitter/ChannelEarlyReturnEmitter,ChildField.ErrorsField.Transport.WrapperArm, theErrorChannelWalkerErrorsub-seal ofAuthorError(four arms + LSPgraphitron.error-channel.codes +typed-rejection.adoc), and two classify-time validator rules (MultipleErrorsFields,NonNullableSuccessProjectionField, the latter the load-bearing rail preventing the silent errors-drop). Arm-switch is an explicit generation-time inline read againstsuccess.value()(delegation prototype rejected by principles review). The wire shape is pinned by theGraphQLQueryTestexecution round-trip (success + mapped-error + unmapped arms), with both rejection rules covered byErrorChannelClassificationTest.nonNullableSuccessProjectionField_rejectsCarrierandOutcomeTypeValidationTest.@tableMethod+ child@serviceflips and the fullPayloadClassdelete are deferred to a follow-up slice (thePayloadClassarm stays live for those paths); DML stays on the sentinel/localContexttransport (R268 owns the arm-switch machinery retirement; R274 the vestigialOutcomeType.successProjection). Supersedes R241, moots R201. Full pipeline green across all tiers.- R268 (fc365b4+ self-reviewf4ae047): Collapse theOutcomearm-switch to a binary fork over reused field resolution. R244 slice 1 introduced a *second switch over theChildFieldtaxonomy:FetcherEmitter.armSwitchValueExprre-derived each variant’s read behind an allow-list (OUTCOME_TYPE_ARM_SWITCHED_DATA_CHANNEL_VARIANTS, nine variants) that drifted from the four the emitter implemented, yielding both a latentIllegalStateException(four allow-listed-but-unimplemented variants) and a false author-error rejection of@table-bound DataLoader data fields (RecordTableField, theopptak-subgraphshape) sibling to the errors field. This retires the parallel taxonomy at the root: the arm-switch now reuses each field’s own read, source-bound fromenv.getSource()tosuccess.value(). Five seams:FetcherEmitter.dataFetcherValueforks three structural roles undersourceIsOutcome(errors field + method-backed DataLoader fields fall through to the raw method reference; inline-resolved data fields arm-switch in place viaarmSwitchedInlineDataFetcher/inlineSuccessRead, covering jOOQ-record columnget, the sharedrecordBackedAccessorRead, and constructor/nesting passthrough);GeneratorUtils.buildRecordParentKeyExtraction+ thebuildFkRowKey/buildLifterRowKey/buildAccessorKey*helpers take a source-bindingCodeBlock;DataLoaderFetcherEmitter.buildgains a pre-registration-prelude overload so the@tablefetcher narrowsSuccessand returnscompletedFuture(null)on theErrorListarm before loader registration (the preferred seam-3 ordering);hasWrapperArmErrorshoisted to one home onFetcherEmitterfor bothFetcherRegistrationsEmitterandTypeFetcherGenerator; andGraphitronSchemaValidator.validateOutcomeChildArmSwitchdeletes the allow-list, replacing membership with a contextual structural invariant (every immediate child of aWrapperArmoutcome type resolves through a graphitron-emitted fetcher, never graphql-java’s defaultPropertyDataFetcher) keyed off the sharedFetcherEmitter.resolvesViaPropertyDataFetcher. R270 (allow-list/emitter reconcile) is moot. The four nested-method variants and DML stay out of scope; R269 (success-arm null-guard) and R271 (dunder sweep) coordinate on the same helper lines. Coverage: pipeline-tierFetcherPipelineTest.outcomePayload_tableDataField_*+outcomePayload_columnDataField_armSwitchesInlineReadOnSuccessValue(classification + wiring kind, no body-string assertions); validation-tierOutcomeTypeValidationTest.outcomePayloadWithTableDataField_isNotRejected(false-rejection fix with classification preconditions); execution-tierGraphQLQueryTest.submitFilmReviewWithFilm_*(new sakilaFilmReviewWithFilmPayloadfixture round-tripping both arms). Full reactor green. -
R283 (
f55b9ac): Emit the@oneOfdirective definition into the federation SDL outputs.ServiceSDLPrinter.generateServiceSDLV2prints the@oneOfapplication but strips the spec-built-in definition (DirectiveInfo.isGraphqlSpecifiedDirective), so Apollo composition rejected the subgraph withUnknown directive "@oneOf". Reinstated on both federation seams from one source of truth: codegen-sideOneOfDirectiveSdl(DEFINITION/usesOneOf/augment) wraps the file arm’sgenerateServiceSDLV2output inSchemaSdlEmitter.printFederationServiceSdl; the generated<outputPackage>.util.OneOfDirectiveSdl(OneOfDirectiveSdlGenerator, wired intoGraphQLRewriteGeneratorunder afederationLink && usesOneOfgate so a non-federation@oneOfschema emits no dead helper) corrects the runtime_Service.sdlby re-printing, appending the definition, and reinstalling aStaticDataFetcher, withGraphitronSchemaClassGeneratorwrapping the federationbuild’s return in `withOneOfDefinition(insideif (federationLink)) under theusesOneOfgate. The one drift-prone string,DEFINITION, is single-sourced from the codegen constant; non-@oneOfschemas keep byte-identical output.@oneOffixture (FilmOneOfFilter) added to the federated and shared sakila schemas. Coverage:SchemaSdlEmitterTest(federation-arm emit + no-op byte-stability guard + plain-arm regression guard),FederationBuildSmokeTest.serviceSdlExposesOneOfDirectiveDefinition(execution-tier{ _service { sdl } }carries application + definition, no errors),OneOfDirectiveGateTest(federation emits the helper; non-federation does not while its plain SDL still carries the definition, pinning thefederationLinkconjunct). R253 cross-reference note added so its controlled-printer route preserves the@oneOfcarve-out. Full reactor green. -
R149 (
609dc70): End-to-end producer-side test thatGraphQLRewriteGenerator.buildOutput()populates both halves ofBuildOutput.report(). NewBuildOutputReportPipelineTest(@PipelineTier) drives a hand-written schema against the test jOOQ catalog carrying two independent diagnostics, an unresolvable@referencekey (→UnclassifiedField→report().errors()) and a redundant@record(→ build warning →report().warnings()), and asserts both halves are non-empty with content-specific matchers, closing the producer-side gap R147 deferred. The spec was narrowed to this one bullet: bullet 1 (end-to-end LSP publish-diagnostics) had already shipped under R196 asBuildTriggerPublishesDiagnosticsTest. NOTE: the R149 work landed under the mislabeled commit609dc70("R266 rework: UK-delete execution proof…"), a rebase artifact that also carries the wrong session trailer;git show 609dc70 --statshows only the three R149 files (the test, the narrowed spec, the README row). Full reactor green. -
R285 (
58a7c8f+d7d2861): Lift-back projection for child@servicefields returning a table-bound type (ChildField.ServiceTableField). AServiceTableFieldwas emitted as a terminal record producer (buildServiceRowsMethodreturning the service result verbatim), so non-column sub-fields on the returned type, the first being a@referencecorrelated multiset, failed at query time withField "<name>" is not contained in row type. The fix routesServiceTableFieldthrough the newSplitRowsMethodEmitter.buildServiceTableLiftrows-method: call the@servicemethod, extract each returned record’s PK, re-project the bound table on that PK by identity throughType.$fields(…), carryparentIdx+seqfor scatter and intra-parent order, and re-wrap into the loader container (List/Map × list/single). The loader value type becomes the projectedorg.jooq.Record, not the developer-returnedXRecord. This is the condensedServiceRecordField → RecordTableFieldshape from the spec’s fork resolution: no new sealed variant, no model change, the FK-hop-vs-identity distinction already lives on the source/key axis (SourceKey.Reader.ServiceTableRecord). AllServiceTableField`s now lift uniformly (scalar-only included), which preserves any service-applied filtering and avoids an emitter-side predicate branch. `TypeFetcherGeneratordispatch assembles the service call and sets the loader value type toorg.jooq.Record. Validator:validateServiceTableFieldgains a return-table-PK guard (identity re-projection needs the returned table’s PK), mirroring the parent-PK check. Coverage: executionGraphQLQueryTest.films_castMembers_referenceSubfieldResolvesViaServiceTableFieldLift(mapped + list,Film.castMembersoverFilmActorwithactor: Actor @reference, asserting exact per-film cast so the reference resolves, no cross-parent leakage, no widening); pipeline both containers (FetcherPipelineTestpositionalloadFilmsreturnsList<List<Record>>+serviceField_mappedContainer_rowsMethodReturnsMapOfProjectedRecord); unitServiceFieldValidationTest.RETURN_TABLE_NO_PK; structural pins updated to the projected-Record loader value (TypeFetcherGeneratorTest,UnifiedEmissionPinsTestskeleton-route count 6 → 7). Drop-out semantics (returned key with no matching row falls out of the identity JOIN) are structurally guaranteed and the no-widening property is asserted, but not separately execution-tested since sakilafilm_actorrows are all real FK rows. Full reactor green. -
R286 (
12a9f88+77362d3): Allow@key(resolvable: false)on non-table-bound types (reference-only federation entity stubs).EntityResolutionBuilder.build()’s second loop (classified-type loop) now skips a non-table-bound type when every `@keydirective on it isresolvable: false(no demote, noEntityResolution): it is a reference-only stub the subgraph declares for the supergraph composer but does not resolve, so it needs no backing table and emits no_entitieshandler. When at least one key is resolvable the R176 table-required diagnostic still fires. The decision turns on the federationresolvableflag alone, never on@record(or any) classification; a reachable stub then rides the ordinary classified-type path into the served_service.sdlcarrying its@key(… resolvable: false). The R276 first-loop (absent-from-registry orphan) rejection is explicitly out of scope: an over-scoped first-loop relaxation shipped at01696e1was reverted at77362d3(orphans never reach the runtime SDL, whichGraphitronSchemaClassGenerator.planForbuilds from the registry, so the relaxation only suppressed the error without surfacing the type). Coverage: unitEntityResolutionBuilderTest.resolvableFalseKeyOnRecordType_isAcceptedAsReferenceOnlyStub+mixedResolvableAndNonResolvableKeysOnRecordType_stillRejects; execution-tierFederationBuildSmokeTest.serviceSdlExposesNonTableBoundResolvableFalseStub(newFilmRefStubfixture, a non-table-bound service-bound record carrier reachable viaQuery.filmRefStubs, asserts the type and its@key(… resolvable: false)reach the served{ _service { sdl } });resultEntityUnionContainsAllFixtureEntitiesupdated to expectFilmRefStubin_Entity(federation-jvm includes every@keytype regardless of resolvability, exactly like the table-boundLanguagestub; benign sinceresolvable: falsegoverns composer routing, andFederationEntitiesDispatchTeststays green with the resolution-less member present). Follow-up R289 filed to correct theKeyNodeSynthesiseropt-out javadoc, which still claimsresolvable: falsekeeps a type out of_Entity. Full reactor green. -
R292 (
4102697+ self-review7a34e4e): Descriptions on synthesised Connection/Edge/PageInfo boilerplate. Graphitron-synthesised relay types carried no SDL descriptions, tripping Apollo’sALL_ELEMENTS_REQUIRE_DESCRIPTIONlinter on every generated Connection/Edge type and field (20+ violations on a real consumer schema). The fix lives entirely inConnectionPromoter, the single synthesis site: thirteen canonical graphql-relay-js wording constants with.description(…)added to the type builder and each field definition inbuildSynthesised{Connection,Edge,PageInfo}. Single source of truth, parity for free: the description rides on the synthesisedGraphQLObjectTypecarried byConnectionType/EdgeType/PageInfoType.schemaType(), so it lands on both published seams (SchemaSdlEmitter’s `SchemaPrinterfile output andObjectTypeGenerator’s runtime rebuild, which reads `getDescription()per type/field) with no second emission site. Generic wording, not parameterised by element type. The structural (SDL-declared) Connection/Edge/PageInfo path is untouched: consumer-owned descriptions, and the SDL declaration is the override lever; the synthesis path only runs when the type is absent from the SDL. Coverage: unitConnectionPromoterTest.directiveDrivenSynthesis_carriesRelayDescriptionsOnTypesAndFields(asserts descriptions onschemaType()and every field against the GraphQL model, including synthesised PageInfo whose fixture declares none); pipelineSchemaSdlEmissionTest.synthesisedConnectionBoilerplateCarriesRelayDescriptions(re-parses the emittedschema.graphqlsstructurally, confirming descriptions survive theSchemaPrinterseam); pipelineSynthesisedConnectionRuntimeDescriptionTest.runtimeRebuiltSchemaCarriesSynthesisedConnectionDescriptions(reads descriptions offGraphitron.buildSchema’s runtime `GraphQLObjectType, a genuine pin of the runtime seam thatFederationBuildSmokeTest.emittedSdlMatchesRuntimeSchema’s `SchemaDiffingcannot see because it does not walk descriptions as graph vertices). The sakila fixture declaresPageInfostructurally, so the pipeline tests scope to the genuinely-synthesisedQueryStoresConnection/QueryStoresEdge; synthesised-PageInfo is pinned at the unit tier. Full reactor green. -
R294 (
36926c4+aa3c772): Treat generator warnings in test fixtures as errors unless asserted. Establishes the policy that fixture builds treat generator warnings as errors unless the fixture’s point is to assert the warning path. Phase 1 (channel unification): the@asConnectionsame-table required-@nodeIdhygiene advisory now ridesctx.addWarning(new BuildWarning(…, locationOf(fieldDef)))instead of the dedicatedASCONNECTION_HYGIENE_LOGSLF4J category, which is retired, so every classifier advisory surfaces onschema.warnings()as one inspectable record;AsConnectionSameTableWarnFormatTestmigrated off logger capture ontoschema.warnings(). Phase 2 (cleanup + gate): removed the 11 redundant@recorddirectives and the redundant@splitQueryfrom the sakila-example schema (their warning paths stay owned on minimal SDL byR96RecordBindingPipelineTestandSingleRecordTableFieldServiceProducerPipelineTest), and addedFixtureWarningsGateTest, which builds the example schema viabuildOutput()and assertsschema.warnings()is exactly the one expected(message, coordinate)advisory: symmetric drift protection (a new accidental warning fails the size assertion, a vanished one fails the content assertion). Open check resolved:filmsConnectionByRequiredIdsexists to prove R113’s production shape (required same-table@nodeId+@asConnection) ships a working WHERE-pk-IN connection, so the shape and its one warning stay; the floor is one expected advisory rather than zero. Out of scope and filed separately: the consumer-facingfailOnWarningMojo feature with typedWarningKindallowlist, deprecated-usage warnings (R296), and R293’s remaining non-generator warning categories. Split out of R293. Full reactor green. -
R287 (
6e3ef42): Remove the DELETE →@tablereturn path. DELETE cannot legitimately project a full@table: the row is gone after the statement andRETURNINGcarries only the primary key, so the old path filled non-PK columns with null, fabricating an entity. The shape is now rejected at classification (author-facing) on two sites:MutationInputResolver.validateReturnTyperejects a direct-return DELETE →@table(closing theMutationDeleteTableField@tablepath so it only ever holds anEncoded*arm), andFieldBuilder.classifyDeletePayloadFieldrejects a@table-element data field on a DELETE payload carrier, both naming why and pointing at the ID return.MutationDeleteTableFieldgains a compact constructor rejecting aProjected*DmlReturnExpressionarm as a runtime backstop (theProjected*arms stay live for INSERT/UPDATE/UPSERT, whose rows survive the statement); the spec considered and declined narrowing the component type via a sub-sealedDmlReturnExpressionon blast-radius grounds. The now-dead carrier and its support chain are deleted:ChildField.SingleRecordTableFieldFromReturning,PkResolution(whoseNonPkNullablenull-fill arm was the wrong behaviour itself),PerFieldOutcome,BuildContext.classifyDeleteTableProjection/DeleteTableProjection/classifyElementFieldForDeleteProjection,FieldClassification.SingleRecordTableFromReturning(+CatalogBuilder/LSP arms),FetcherEmitter.buildSingleRecordTableFromReturningFetcherValue(+ dispatch), theTypeFetcherGeneratordispatch entry, and theGraphitronSchemaValidatorno-op arm.SingleRecordIdFieldFromReturning(encoded-PK-off-RETURNING, deletion-safe) stays. Coverage: validation-tierMutationDeleteTableFieldValidationTestpins both author-facing rejections SDL-driven (no hand-built illegal field); the corpus retires the DELETE@tableverdict, repointing DELETE roots to[Dml]/Columnencoded-ID returns and dropping the DELETE payload examples (covered under the nodeidfixture, carried inVariantCoverageTest’s `NO_CASE_REQUIREDforMutationDeletePayloadField/MutationBulkDeletePayloadField);LeafTupleAdapter’s refusal arm and `PkResolutionEmitterReachabilityTestretire with the type; the sakila example dropsdeleteFilmsTableCarrier/DeletedFilmsTablePayload/DeletedFilmInfoand the execution proof, keepingdeleteFilmsIdCarrier. User docs (code-generation-triggers.adoc), model/generator javadoc, and roadmap cross-references corrected so nothing presents DELETE →@tableas designed. Discovered during R281 dimensional-model design and independently flagged by the 2026-06-10 staleness audit. Full reactor green. -
R301 (
3802964+d0e8dde+d99b9ab): Align docs and javadoc with the R276@recordremoval.@recordis now parsed-but-ignored: the backing Java class is reflection-derived from the producing field (an@servicereturn, or parameter for inputs; a@tableresolution; a@tableMethodreturn; or a parent-accessor chain), and a reachable type still carrying it warns to remove it (redundant / shadowed-by-@table/ disagrees-with-reflection, the three variants emitted atTypeBuilder.emitDirectiveIgnoredWarnings). The reference pagerecord.adocis rewritten as a deprecation/ignored page;deprecations.adoc+ both directive indexes mark@recordignored (precedent:@index);directives.graphqlsreframes the@recorddescription as DEPRECATED/IGNORED and drops@recordfrom theargMapping-inert list (matchingcheckArgMappingInert, which no longer fires for it); explanation pages, how-to guides, and the rewrite-internal adocs shift from@record-bound/-declared/-parentto reflection-derivedclass-backed/record-backedterminology with@record(record:)stripped from every worked example in favour of the producing field; javadoc acrossmodel/,catalog/, and generator classes reworded comment-only (the lone code change addsrecordtoDeprecationsDocCoverageTest.WHOLE_DIRECTIVE_DEPRECATIONS). The generatedsupported-schema-shapes.adocwas regenerated from the updated leaf javadocs (also resyncing pre-existing drift:PlainObjectTyperemoved per R276, newer leaves added);leaf-coverage --verifyreports up to date. Runtime diagnostic strings (@record parent,a @record type) left untouched as out of scope. Full reactor green. -
R312 (
2524d8c): ThreadCompositeDecodeHelperRegistrythrough the inline/split reference-field filter emitters, fixing a codegen crash when a filter input on a reference/list child field mixed@nodeId-decoded fields with@conditionfields. Part A: own-and-drain a per-class decode registry at the two class-assembly points that host reference-field filter sites;TypeClassGeneratorowns one registry per<Type>class (threaded throughbuild$FieldsMethod→emitSelectionSwitchincluding theNestingFieldrecursion intoInlineTableFieldEmitter/InlineLookupTableFieldEmitter),TypeFetcherGeneratorone per<Type>Fetchersclass (threaded intoSplitRowsMethodEmitter.buildFor*andbuildQueryLookupRowsMethod). A newCompositeDecodeHelperRegistry.collectInto(TypeSpec.Builder, Consumer<…>)bracketing helper co-locates construct and drain so a lifted helper can never be silently dropped, withQueryConditionsGeneratorrefactored onto it so there is one drain implementation. Part B: guard the empty-join-path (standalone-lookup) shape uniformly withParentCorrelation.checkCarrierInvariant;InlineTableFieldEmitteremits a correlation-free conditions-only subquery (synthetic terminal alias + pre-switchDSL.noCondition()seed),InlineColumnReferenceFieldEmitterprojects the column directly off the parent alias, andSplitRowsMethodEmitterthrows a descriptive classifier-invariant error instead of an opaqueIndex -1. TheArgCallEmitter:372null-registry throw is kept as the backstop. Coverage:NodeIdReferenceFilterPipelineTest(inline + split lift asserting the liftedprivate static decodeBar*helper, condition-only real-FK regression guard, empty-join-path standalone) plus agraphitron-sakila-examplecompilation-tier fixture (FilmMixedNodeIdConditionFilteron inline and@splitQueryreference fields) as the cross-module forgotten-drain backstop. No newSTUBBED_VARIANTSentry; thePROJECTED_LEAVES"fully implemented" claim stays honest. Full reactor green under-Plocal-db. -
R313 (
adfaeff+ build-through43645d9): Fix@scalarType/ convention scalars registering under the constant’s intrinsic name instead of the SDL name. A scalar whose SDL name aliases the constant it resolves to (the canonical casescalar LocalDate @scalarType(scalar: "graphql.scalars.ExtendedScalars.Date"), whose constant is namedDate; and the convention siblingGraphQLBigDecimal, whose constant is namedBigDecimal) was emitted asadditionalType(<constant>), registering graphql-java under the constant’s name, so everytypeRef(<sdlName>)bound to nothing and the generatedGraphitronSchema.build()threwtype <SdlName> not found in schemaat runtime (surfaced downstream as Sikt’sSakMerknaderShapeTestbuild failures). The fix routes the mismatch through the existingScalarResolution.Synthesisedarm rather than teaching the emitter a new branch:ScalarTypeResolver.resolveFromConstantFqngains an SDL-name-aware overload that forks oncheck.scalar().getName().equals(sdlName)(match →Resolvedas before; mismatch →Synthesised(javaType, sdlName, owner, field), borrowing the constant’s coercing), with the SDL name threaded in viaresolveFromDirectiveValue/resolveByConvention(built-ins never alias, soresolveBuiltInis untouched);TypeBuilder’s `@scalarTypeand convention arms widen frominstanceof Resolvedtoinstanceof Successful, mirroring the federation arm. No new model component, no new emitter branch, no.javaType()reader change. Coverage spans all three tiers: resolver-tierScalarTypeResolverTest(alias →Synthesised, match →Resolved, theGraphQLBigDecimalsibling, and the convention-loop widened toSuccessful); pipeline-tierGraphitronSchemaClassGeneratorTest(alias emits thescalar_LocalDate()synthesised helper, not the bare constant;Moneyno-regression keeps the plainadditionalType(…MONEY)form);GraphitronSchemaBuilderTest.DIRECTIVE_BEATS_CONVENTIONcorrected to theSynthesisedoutcome (latently broken before); and the load-bearing execution / build-through added on review (43645d9): agraphitron-sakila-examplefixturescalar LocalDate+Customer.createDateon the realcustomer.create_dateDATE column, withgraphql-java-extended-scalarsat compile scope, whoseGraphQLQueryTest.aliasingScalar_registeredUnderSdlNameAndResolvesEndToEndasserts the assembled schema registersLocalDate(andDatedoes not leak) and projects ISO date strings end-to-end against PostgreSQL, reproducing the runtime failure on pre-fix code. Full reactor green under-Plocal-db. -
R317 (slices 1–5, collapse
c084745+ immutable-validateb0f4305/6930c25, In Reviewd347cca): Single edge-driven classification pass and immutable validation;TypeBuilder.buildTypesretired. R279 left the reachable surface traversed three times (aSchemaReachabilityname-set walk,buildTypes’ eager type loop, `buildSchema’s field loop) with the real verdict for directiveless objects scattered across three post-passes (`promoteSingleRecordPayloads,registerNestingTypes, the orphan arm ofrejectDanglingTypeReferences). R317 collapses all three into oneSchemaReachability.walkdriving a realGraphQLTypeVisitor(GraphitronSchemaBuilder.ClassifyingVisitor) that classifies each composite on enter and folds its fields' classification into the same visit, governed by the read-free visitor invariant (the classifying visit may onlyregister, never read the registry under construction). The three scattered post-passes fold onto the producing/embedding edge as registry-free verdicts (carrierTableBinding,isDirectivelessNestingTarget, the slice-3c edge orphan); the two reverse-lookups become pure typename-keyed fixed-point indices (ctx.tables/ctx.nodes/ctx.errors) threaded as traverser arguments; target-verdict reads at field edges go through a registry-freeTypeBuilder.lookAheadVerdict(forced by graphql-java 25’s enter-only traversal, where a field’s output target is a not-yet-visited child).buildTypes, thereachableOutputTypeshand-off, and the field loop are deleted. Slice 5 inlines R318: the five global soundness reductions (node-typeId uniqueness, case-fold collisions, the dangling backstop, federation@key, multi-producerDomainReturnTypeagreement) now register aValidationErroron a singleGraphitronSchema.diagnosticschannel (viaBuildContext.addDiagnostic) the validator drains, instead of demoting a settled verdict toUnclassifiedType/UnclassifiedField; a verdict read after the walk equals the verdict classification produced, and theValidationErrorstream / which schemas pass or fail stay byte-identical. TheNodeIndexis one-to-many by table (a table may back several@nodetypes); implicit-encoder ambiguity moved to a use-site rejection with a disambiguation hint, correcting the oldfindFirst()arbitrary pick. The field-relative input model split to R327 (the one non-byte-identical change); R319 (warn-on-prune) stays separate. Coverage: the falsifiable acceptance testSingleWalkClassificationOrderTest(a deep target’s type-classify trace follows its discovering field’s, which an eager type pass fails) plusNodeIdPipelineTest(MULTIPLE_NODE_TYPES_PER_TABLE_ALLOWED,TYPE_ID_COLLISION_DEMOTES_BOTH),MutationDmlNodeIdClassificationTest.idReturnOnMultiNodeTable_ambiguous_rejected,GraphitronSchemaBuilderTest(SERVICE_MUTATION_ID_CARRIER_UNBOUND_ORPHAN_REJECTED_AT_EDGE, the orphan/case-fold cases now asserting the verdict stays real),EntityResolutionBuilderTest, andAppliedDirectiveEmitterTest(FEDERATION_SDLgiven a@tablesoUserclassifies). Truth table 448; folds in and discards R325 (read-free visitor restatement). Full reactor green under-Plocal-db(execution tier 413 tests). -
R331 (
f912d7f): Scope LSP@field(name:)validation/hover/completion on@table-interfaceparticipant cross-table reference fields to the@referenceterminal table.FieldClassification.ParticipantCrossTablewas the one column-bearing permit still in theFallThrougharm oflspColumnDispatch(), so the three column-name LSP consumers dispatched on the enclosing participant@tablerather than the terminal table; a single-table-interface participant field reaching a column on another table via@referencedrew a false-positiveUnknown column … on table '<participant table>'squiggle on a schema that builds clean, plus wrong-table hover and completion. Fix is a single-arm relocation toResolve(c.targetTableName()), the same routing R224/R233 gave the four other column-bearing permits; the record stays distinct for the FK-constant/alias hover surfaces (DeclarationHovers/InlayHints/LspClassificationLabels), which pattern-match it directly and are unaffected. Coverage mirrors the R233 trio with the interface-participant dimension:DiagnosticsTest(valid column → no diagnostic; bogus column →Unknown column 'NOPE' on table 'language', never citing the participant table'film'),HoversTest, andFieldCompletionsTest. Full reactor green under-Plocal-db. -
R330 (
8197af1+84102d6+06b47de+e9d80fc): Fix@condition(override: true)on a@nodeIdFK-target filter field passing the parent’s root table instead of the joined FK-target alias, a v9→v10 parity gap surfacing asincompatible typesat consumer compile (e.g. aSoknadsmangeltypehanded toiRegelverksamling(Regelverksamling, …)). An FK-target@nodeIdfield’s developer@conditionmethod expects the FK-target tableXreached through a foreign-key join path, not the input’s own table, but the rewrite’s no-joinliftedSourceColumnsmodel never propagated the join into the@conditionmethod’sParamSource.Tableslot, so the emitter passed the literal"table"for every condition method. The model gap is lifted into a sealedWhereFiltersiblingFkTargetConditionFilter(alongsideConditionFilter/GeneratedConditionFilter) carrying the targetTableRef, the resolvedFkJoinjoinPath, the lifted FK-child source columns, andX’s key columns; `FieldBuilder.walkInputFieldConditionswraps both the single-columnColumnReferenceFieldand compositeCompositeColumnReferenceFieldarms in it whenever the join path is non-empty. Every WHERE-emitting site forks on the type through a sharedFkTargetConditionEmitter(declareAliases+emitTerm) so the FK-target arm is defined once and cannot drift across the five sites (QueryConditionsGeneratorshim,InlineTableFieldEmitter,InlineLookupTableFieldEmitter,SplitRowsMethodEmitter,TypeFetcherGenerator.buildQueryLookupRowsMethod); the plain arm stays byte-identical. The FK-target arm emits a correlatedDSL.exists(DSL.selectOne().from(X).where(<correlation>.and(method(X, args))))(approach B over restoring the legacy top-level join, keeping the(Table, env) → Conditionshim contract and staying inside theJoinStepcardinality invariant); the correlation reusesJoinPathEmitter.emitCorrelationWhere, which ANDs every FK slot, so composite-key FK targets work for free with noRowN. Recursing inline/lookup/split sites runtime-prefix their SQL aliases onto the base alias’sgetName(); the two top-level method sites use static aliases. Both reported instances fixed (SoknadsmangeltypeFilterInput.regelverksamlingIdshim path in pass 1;EndringsloggV2FilterInput.brukerIdinline child path in the rework). Composite-key NodeType targets are now supported rather than deferred to R24 (they are the common consumer shape); the validator’s composite rejection narrowed from a blanket deferral to the same non-FkJoin-hop guard the single-column case uses, mirroring the emitter precondition. The per-argument nested-ternary extraction readability work was split out to R334. Coverage: pipelineNodeIdOverrideConditionFkTargetPipelineTest(single-column + composite FK-target carrier assertions, no code-string assertions on method bodies); sakila compile-tier guards (concreteAddress/Projectcondition parameters) and execution assertions for shim, shim+field-override, inline child,@splitQuerychild, multi-field shim, composite@table, and plain-input composite on both list and@asConnection. Full reactor green under-Plocal-db. -
R338 (
f40b056+ warnings-gate371a5eb): Split-query correlation now keys both cardinalities off the FK’s referenced columns instead of the parent PK. A list@splitQueryreference field whose@referenceFK targets a non-PK unique key on the parent silently returned zero rows for every parent:FieldBuilder.deriveSplitQuerySourcebuilt theparentInputVALUES table from the parent’s PK columns on the List (child-holds-FK) branch, whileSplitRowsMethodEmitter’s correlation predicate references the FK’s actual referenced columns (`sourceSideColumns()); when those are not the PK,parentInput.field(…)resolved tonulland the predicate degraded tocol = NULL, matching nothing with no error raised. The fix drops the!isListguard so both cardinalities key off the first hop’ssourceSideColumns()when the first hop is anFkJoin(BuildContext.resolveFkSlotsalready orients a child-holds-FK first hop so the slot’s source side is the parent’s referenced columns), keeping theprimaryKeyColumns()fallback only for the non-FK first-hop (ConditionJoin) shape, whereParentCorrelation.OnConditionJoincorrelates on parent PK. The read-side machinery already reads arbitrary FK source columns off the parent record (the Single branch andderiveFkRecordParentSourceprove this), so no emitter change was needed; the stale parent-PK-assumption javadoc was rewritten. Coverage: execution-tierGraphQLQueryTest.splitTableField_fkReferencesNonPkUniqueKey_returnsChildRowsover a newsplit_parent(PKparent_id, UNIQUEparent_code) +split_parent_tag(FK →split_parent.parent_code) fixture, asserting the child rows scatter per parent by the unique-key value (ALPHA two tags, BETA one) and the batch fan-in stays at two round-trips, behavior-asserted with no code-string assertions; verified to fail (empty list) with the fix reverted.FixtureWarningsGateTest’s pinned schema line updated for the added Query field. Full reactor green under `-Plocal-db. -
R339 (
cc45dbb): Honour@defaultOrderdirective-leveldirection:on theprimaryKey:andindex:variants.OrderByResolver.resolveOrderEntriesalready threaded the resolveddefaultDirectioninto thefields:branch but hardcodedSortDirection.ASCon the two sibling branches, so@defaultOrder(primaryKey: true, direction: DESC)(and theindex:variant) silently sorted ASC, violating the directive’s published contract (direction: SortDirection = ASCdeclared directive-level with no per-source carve-out). This reverses R243’s "fork (b)" for@defaultOrderonly: theprimaryKey:branch now stampsdefaultDirectiononto each synthesised PK entry, andresolveIndexColumnstakes aSortDirectionparameter that the@defaultOrdercall site feedsdefaultDirectionwhile the@orderenum-value alias still passesASC(its direction comes from the runtime input object’sdirection:field, flipped in the*OrderByhelper at code-generation time, not the directive). The directive-absent implicit-PK fallback inresolveDefaultOrderSpecstays ASC. No emitter or seek change was needed:uniformAsc, emission (jooqMethodName()), and keyset seek already derive from per-column direction, so an all-DESC PK/index default yieldsuniformAsc == falseand paginates descending end to end automatically. Coverage: pipeline-tierGraphitronSchemaBuilderTestrewroteDEFAULT_ORDER_DIRECTION_DESCin place (no stale fixture left alongside) to assertuniformAsc() == false/direction() == DESC, and addedDEFAULT_ORDER_INDEX_DESC; execution-tierGraphQLQueryTest.filmsConnectionDesc_executesDescendingPrimaryKeyOrderover a newfilmsConnectionDescconnection assertsfilmIdorder 5..1 (the exact reverse of the PK-ASC baseline), proving emitted.desc()+ descending keyset seek with no code-string assertions;FixtureWarningsGateTest’s pinned schema line updated for the added field. Full reactor green under `-Plocal-db. -
R90 (
fa07632Phase 1+2 +2dd6fbbPhase 3 +e066346Phase 4 + docs6d3ac86): LSP Java-source surfacing for goto-definition, Javadoc hover,@externalFieldcompletion, andargMapping. Framed as expanding the catalog dataCatalogBuilder.buildalready exports to the LSP, not a new feature: theSourceLocation/descriptionslots existed but the jOOQ half exported only file-level (0:0) positions and the service half (ExternalReference/Method, bytecode-only) carried no source location or Javadoc at all. NewSourceWalker(parse-only JDK Compiler Tree API, no external dependency; per-file mtime cache so a.class-only watcher trigger re-parses nothing) recovers declaration positions and Javadoc from the consumer’s compile source roots, threaded throughRewriteContext.compileSourceRoots(populated fromMavenProject.getCompileSourceRoots()inAbstractRewriteMojo).CatalogBuilderis the single join site: it rebuilds the immutableTable/Column/ExternalReference/Methodrecords from theClasspathScannerstructure plus the walk index in one pass (jOOQ half refined to per-line + field Javadoc; service half gains aSourceLocationcomponent withUNKNOWN-defaulting back-compat factories), keying methods on(className, methodName, paramCount)and dropping overload-ambiguous keys toUNKNOWNrather than binding a wrong line.Definitions.computegains a service-half arm reusingLspVocabulary.behaviorAt/siblingStringAtfor@service/@externalField/@enum/@condition/@sourceRow/@tableMethod(@recordcarved out, mirroring completion/hover);Hoversrenders class/method Javadoc;ExternalFieldCompletionsnarrows the method list to single-parameterField-returning lifters;ArgMapping(pure string-content decomposition) +ArgMappingCompletions+argMappingdiagnostics cover thejavaParam: graphqlArggrammar (left = method parameter names, right = enclosing field’s GraphQL args, head-segment only for R84 dot-paths). Two documented in-scope approximations bounded by the spec’s out-of-scope list: the@externalFieldTable-parameter check uses the catalog-derivable signature shape (the classifier-drivenParameter.source = ParamSource.Tableprojection is generator-side work the LSP catalog does not carry), andargMappingdot-paths validate/complete the head segment only (the snapshot carries no nested input-field projection for arbitrary input types). Coverage: LSP-tierDefinitionsTest(one case per binding directive +@recordcarve-out + unknown-name / overload-UNKNOWNfall-throughs), pipeline-tierCatalogBuilderSourceTest(column + service-half refinement and Javadoc lift from a synthetic source root;UNKNOWNfallback when roots absent), unit-tierSourceWalkerTest(overload-ambiguity, doc-comment retention, unparseable-file tolerance, mtime cache invalidation, no params/locals as fields), plusArgMappingTest/ArgMappingDiagnosticsTest/ArgMappingCompletionsTest/ExternalFieldCompletionsTest/HoversTest; no code-string assertions (no generated output, positions /Location`s are the asserted shape). `getting-started.adocdocuments the editor surface. Predecessor R18. Full reactor green under-Plocal-db. -
R343 (
72440a0a0): LSP column-name completion / hover / validation for@defaultOrder(fields: [{name: …}]). Binds theFieldSort.namecoordinate toBehavior.CatalogColumnBindingin theLspVocabularycanonical overlay; previously the site fell through toArgNameCompletionsand offered no column suggestions, nudging authors to hand-write an ordering condition resolver instead of the declarative@defaultOrder. The crux was which table’s columns to offer: a list/connection field’s ordering columns live on the navigated (element) table, not the enclosing type’s@table. Resolved in the classification rather than the LSP, so all three surfaces agree on the terminal table, by relocatingTableTarget/RecordTableTargetfrom theFallThrougharm ofFieldClassification.lspColumnDispatch()toResolve(tableName())(the element table), the same single-arm pattern R331 gaveParticipantCrossTable; the@reference-backed shape already resolved viaParticipantCrossTable. No new completion provider. Coverage: pipeline-tierLspColumnDispatchProjectionTest(plain list,@asConnection @splitQueryconnection, and@splitQueryshapes all classifyTableTargetand dispatchResolve(element-table)),FieldCompletionsTest(element-table columns offered not the enclosing type’s, across plain / connection /@reference+@splitQuery; negative for theprimaryKey:site),HoversTest+DiagnosticsTest(hover and column validation cite the element table; a bogus column reported on it), andDriftDetectionTest(FieldSort.nameresolves and binds to the column behavior under the startup invariant); no code-string assertions. Builds on R119 and R233. Full reactor green under-Plocal-db. -
R356 (
27fc6d4, Spec5e9ae41): Unify the per-column shared-column overlap analysis across the six accreted DML mutation write-path sites onto one shared primitive. The "group writers by backing column, keep size-two-or-more, an all-plain overlap is a build-time reject and a decode-involving one needs a runtime value-agreement check" grouping was hand-rolled in six places (R322/R354/R342). Newmodel/ColumnOverlapintroduces a minimal read-onlyColumnWriterview (targetColumns()in decode-record slot order,decode(),label()), aContributor, anOverlapColumn(shared()/allPlain()), andgroupByColumn; a pure structural fold over already-resolvedsqlNamevalues invoked once per site, not a model-carried fact (the@mutationvalidator runs at resolution time, before the emit carriers exist, so a per-carrier stored fact would force the validator to keep its own walk).JooqRecordInstantiationEmitter(site 1) retiresanalyzeOverlap+SlotRef, adaptingWriterinto the view;TypeFetcherGenerator’s `insertColumnPlan(site 2) andsetColumnPlan(site 6) delegate togroupByColumn, retiringInsertCol/InsertColWriterand R342’s cloneSetCol/SetColWriterontoOverlapColumn/Contributor,emitSetAgreementPreamble(site 4) replaces its inlinebyColumnmap /int[]tuples, sites 2 and 4 route their value-read through the sharedappendAgreementValue/emitAgreementDecodeLocalseam (already serving sites 5 and 6), andemitKeySetAgreementPreamble(site 5) adopts theSetGroupWriterleaf view + typed records while keeping its bespoke cross-partition intersection;MutationInputResolver(site 3) readsshared() && allPlain()off the same fold, making validator-mirrors-classifier structural. Each emitter downcastsContributor.writer()back to its site view to reach the wrapped carrier. The doubly-stale R342 comment is corrected. Pure refactor: directive, model-carrier, wire-format, the sharedrequireColumnAgreementpredicate, dispatch partitions and theRejectiontaxonomy are untouched; site 1’s optional value-read fold and the outer gather-and-pairwise loop stay out of scope as specified. New unit-tierColumnOverlapTestpins the grouping (encounter order, every column kept,shared()/allPlain(), the slot-ordering invariant); the inherited execution + pipeline net (NodeIdValueAgreementExecutionTest,SelfFkNodeId{Insert,Update}ExecutionTest, the R342 bulk cases,MutationDmlNodeIdClassificationTest,JooqRecordServiceParamPipelineTest,UpdateRowsWalkerTest,RejectionSeverityCoverageTest) stays green with no assertion edits. Builds on R322/R354/R342/R328. Full reactor green under-Plocal-db. -
R353 (
951aed0): LSP goto-definition from an SDL declaration name (a type name or a field / input-value name, not a directive argument) to the Java the model bound it to, the navigation handle the cursor naturally rests on and the only handle reflection-bound types carry. Newdefinition/DeclarationDefinitionsdispatches on the enclosing type’sTypeBackingShapevia an exhaustive switch with nodefault(mirroringDefinitionsoverBehavior, so a future backing permit forces a goto-def decision at compile time), resolving every arm through the sealedDefinitionTargetand the LSP-ownedSourceWalker.IndexR349 established: a type name jumps to the backing class (jOOQ table class for table-bound types; the consumer class for reflection-bound record / POJO / standalone-jOOQ types), and a field name jumps member-precise to the backing member (a jOOQ column, a record component indexed as a field by the parse-only walk, or a POJO bean accessor), with a field on a standalone jOOQ record degrading to its backing class and the@field(name:)override read off the field node to name the bound member. The shared declaration-name trigger is factored out ofDeclarationHoversintoparsing/SdlDeclaration(sealedTypeName/FieldName) so the hover and goto-def triggers cannot drift;DeclarationHovers.findContainingbecomes a thin adapter over it. Corrected the signed-off D1’s inverted record/POJO premise: resolution is by source-index key, andMemberSlot.name()is the bean property name a POJO method index is not keyed by, so the slot is widened to carry the arity-0accessorMethodName, populated at the oneCatalogBuilderprojection site (projectPojopassesmethod.name(),projectRecordpassesrc.name()) so the bean rule keeps its single home and both axes are member-precise; this retires the deferred "member-precise record components" follow-up.Definitions.fieldTarget/resolvewidened to package-private for the sibling provider; chained intoGraphitronTextDocumentService.definitionvia a third.or(). Coverage: pipeline-tierDeclarationDefinitionsTest(one case per backing shape per axis: table-class / record / POJO / standalone-jOOQ type names, theSourceAbsent→ empty degrade, table column,@field(name:)-overridden column, POJO accessor method, record component, standalone-jOOQ field degrade, unknown member,NoBacking, directive-argument non-trigger, unavailable snapshot),MemberSlotcall sites updated acrossFieldCompletionsTest/HoversTest/DiagnosticsTest; no code-string assertions (resolvedLocation`s are the asserted shape). Builds on R349 and R90. Full reactor green under `-Plocal-db. -
R366 (
d71545d, In Review143a155, Specb6a6f93/88e61592): EmitloadManydispatch for list-cardinality polymorphic@splitQueryon record-backed parents.MultiTablePolymorphicEmitter.buildBatchedListFetcherunconditionally emittedreturn loader.load(key, env), but for anAccessorCall/MANY(orProducedRecordRead/MANY) parentSourceKey,GeneratorUtils.buildRecordParentKeyExtractiondeclares a loop-localList<…> keysrather than a single method-scopedkey, so the generated fetcher referenced an out-of-scope local and failed javac (cannot find symbol: variable key) on a field that passedgraphitron:validate. This is the polymorphic sibling of the already-fixed non-polymorphic wrapper split-query compile bug. Fix forks the load site onparentSourceKey.cardinality(), mirroring howTypeFetcherGenerator.buildRecordBasedDataFetcherbranchesloadvsloadMany:ONEkeepsloader.load(key, env);MANYemitsloader.loadMany(keys, Collections.nCopies(keys.size(), env))then concats the one-bucket-per-elementList<List<Record>>into the field’s flatList<Record>viaflatMapbefore the async tail (flatten matches the flat[Type!]!surface; the per-element grouping the SDL doesn’t ask for is deliberately not preserved). No model change (cardinality is on theSourceKeythe fetcher already holds) and no new floor-guarantee rejection:AccessorCall/ONEis unreachable on a list field (FieldBuilder.collectAccessorMatchesrejects a single-record accessor asCardinalityMismatch) and single-cardinality Pojo is already deferred-rejected, so the two live list-arm paths areColumnRead/ONE(table parent) andAccessorCall/MANY(record parent), both now compiling. Coverage: compilation-tier fixtureOccupantsBatchPayload(free-form@recordexposingList<AddressRecord> addresses(), childoccupants: [AddressOccupant!]! @field(name: "addresses")over theCustomer | Staffunion) +OccupantsBatchPayloadService+Query.occupantsBatch, whose generatedOccupantsBatchPayloadFetchers.occupantsemits theloadMany+flatMapdispatch and fails javac on any regression toload(key); the pipeline tier already classifies theAccessorCall/MANYshape. Sibling of R367 (single-cardinality guard); sharesMultiTablePolymorphicEmitterwith R363. Full reactor green under-Plocal-db(:graphitron2216 tests,:graphitron-sakila-example455 tests). -
R364 (
172016a, In Review9eff0e4): Fix the@service @splitQueryrows-method return type for enum and non-built-in scalar leaf fields. A non-root@servicechild field whose GraphQL type is an enum (or any scalar outside the five GraphQL spec built-ins) generated a doubly-nestedMap<KeyRecord, Map<KeyRecord, V>>rows method instead of the flatMap<KeyRecord, V>, so the generated code did not compile; siblingInt/Booleanfields were already flat. Root cause:ServiceRecordField.elementType()fell back to the service method’s wholeMap<K, V>whenRowsMethodShape.strictPerKeyTypereturnednull(which it does for any nameScalarTypeResolver.builtInJavaTypecan’t resolve), andouterRowsReturnTypethen wrapped that map once more. Per the spec’s recommended option (a), the fix addsRowsMethodShape.perKeyFromOuter; the structural inverse ofouterRowsReturnTypethat peels the per-keyVback out of a known outerMap<K, V>/List<V>across the(isMapped, isList)cross-product, returningnullfor an unpeelable shape ; and routeselementType()through it for the non-built-in scalar leaf (other null-perKeycases keep the legacy whole-type fallback); both call sites deriveisMappedfromsourced.container()(stored verbatim intoLoaderRegistrationatFieldBuilder.buildServiceLoaderRegistration) so emitter and validator cannot disagree. Spec step 2 closes the validator gap:ServiceDirectiveResolver.validateChildServiceReturnTypeno longer skips the non-built-in scalar case, instead peeling the leaf, reconstructing the expected outer shape, and rejecting a wrong key type / missing list-nesting / unpeelable container at classify time rather than leaving it to miscompile (a self-consistent leaf peel, honestly documented as a key-type-plus-container check, not full strict-equality). The deferred typing-fidelity follow-up (emit-text-mapped-enum-fields-as-enum-type) stays out of scope: the leaf is accepted as whatever the method yields. Coverage: unit-tierRowsMethodShapeTest(forward/inverse round-trip across the full(isMapped, isList)cross-product plus three null-rejection cases), pipeline-tierFetcherPipelineTest(enum-leaf mapped field emits the flatMap<Row1<Integer>, String>with theIntsibling unchanged; wrong-container field rejected at classify time), andTestFilmServicefixtures; no code-string assertions on generated method bodies.graphitronandgraphitron-sakila-exampletiers green under-Plocal-db(execution tier 455 tests);graphitron-lspnot exercised at review (nativelibtree-sitterunavailable in the review sandbox, a known environment gap). -
R371 (
1327de0+ self-review259ff2d): Declaration-name hover now overlays the bound jOOQ class / column / member Javadoc beneath the classification block, closing the asymmetry R369 exposed (goto-definition jumped into the jOOQ source on an SDL type-name / field-name token, but hover on the same token stayed classification-only and never read the source index). The fix makes hover/goto parity structural rather than asserted: a newparsing/DeclTargetsealed family (CatalogTable/CatalogColumn/SourceClass/SourceMethod/SourceField/None) is the one backing-switch from an SDL declaration coordinate to a named jOOQ / Java declaration, and the two consumers each project it exhaustively ;DeclarationDefinitions.locateto aLocation(goto),DeclarationHovers.overlayto a Javadoc string (hover) ; so they cannot point at different declarations and a newTypeBackingShapepermit breaks both switches at compile time. This collapsed the request-time backing-switch from three hand-rolled copies to two (the directive-value@field(name:)armHovers.columnHoverstill runs its own switch and still diverges on the F1/F3 cases; retiring it rides on the candidate follow-up that liftsDeclTargetontoBuilt). Standalone jOOQ records overlay their class Javadoc where goto jumps (spec F1), POJO accessors overlay the arity-0 method Javadoc and record components the component field Javadoc (F3), and onlyNoBacking.*yields neither; the table / column arms keepDescriptions’s SQL-comment-wins precedence. Goto behaviour is unchanged: `locatereproduces the prior per-armDefinitions.resolvecalls exactly. Two spec-stated deviations, both justified:Descriptions.classJavadocwas promoted public rather than package-private (hover is a sub-package, so package-private would not reach it; the access shape F2 itself offered first), andlocate/overlayare public test seams so the parity property is assertable without a tree-sitter round-trip. Coverage: tree-sitter-free unit-tierDeclarationHoverOverlayParityTestasserts the resolver per backing, the overlay text per variant, and the overlay-presence ⟺ jump-presence drift guard perDeclTargetvariant (F4); the liveDeclarationHoversTeststays classification-only via the back-compat 3-arg entry. No code-string assertions on generated bodies. Reviewed independent-session;DeclarationHoverOverlayParityTestgreen (5/5) and thegraphitron-lspreactor (-am) fully test-compiled under-Plocal-db; the live tree-sitter LSP tier was not exercised because the nativelibtree-sitterruntime is egress-blocked in the review sandbox (a known environment gap, the same one the implementer documented), and the diff’s live-path change is a minimal one-call delegation through the back-compat seam. Builds on R369 / R353 / R352 / R90 / R160. -
R368 (
938bb69): MCP structured read-tools over the liveWorkspace, landing R118 slices 3-6 as thin reads on the R361 seam. Five tools plus one resource registered the waystatusToolis:services/conditions/recordsoverWorkspace.catalog().externalReferences()joined withWorkspace.sourceIndex();schemaoverWorkspace.snapshot()joined same-cadence with@nodemetadata offcatalog().nodeMetadata()(exhaustive switches over theTypeClassification/TypeBackingShape/FieldClassification/LspSchemaSnapshotpermits, nodefault);diagnosticsoverWorkspace.validationReport()reporting snapshot availability/freshness alongside; and adirectivesMCP resource (the newresourcescapability) composing the frozen bundled grammar with the live snapshot’s user-declared directives. Two owned, additive shared-model widenings, both via back-compat constructors so existing LSP/test callers compile unchanged:CompletionData.Methodgains a typedreturnsConditionfact classified at the parse boundary inClasspathScannerfrom the un-erased return descriptor (exactLorg/jooq/Condition;compare, so a consumer’s own type namedConditionis not mis-tagged), andDirectiveShapegains an applicable-locationsfield projected atCatalogBuilder.buildSnapshotfromDirectiveDefinition.getDirectiveLocations(). Shared wire mechanics (lenient arg coercion, opaque base64 page cursors, thefqcn#method/aritystable-ID grammar slice 7 will walk, and the typedSourceJoinleft join ; sealedResolved/NotIndexed/Ambiguous, never a silent drop or hard failure) factored intoMcpWire; the R362 catalog tools route through it too. No new generator branch and no validate-time arm: these are descriptive discovery reads, so validator-mirrors-classifier does not apply (re-derived in the spec given the scanner lift). Coverage: unit-tierClasspathScannerTest(the parse-boundary condition classification incl. the false-positive guard),CatalogBuilderSnapshotTest(directive-locations round-trip + back-compat empty default), and MCP-handler tier inGraphitronMcpServerTest(services/conditions/records structured shapes with method refs and resolved/not-indexed location arms, the@nodecatalog join, schema paging + unavailable-before-build, diagnostics mapping + severity filter + snapshot-freshness, the directives resource listing bundled + user-declared with rendered locations, and the stable-ID/join-key round-trip) ; structured-content assertions, no code-string assertions on generated bodies. Builds on R361; sibling of R362. Independent-session In Review → Done review; full reactor green under-Plocal-db(ClasspathScannerTest 14, CatalogBuilderSnapshotTest 15, GraphitronMcpServerTest 21). -
R148 (
7fc5f5e): Re-anchor LSP validator diagnostics off the doc block onto the definition name. graphql-java anchors a described definition’sgetSourceLocation()at the opening delimiter of its documentation block (the description is the AST node’s first token), so an R147 validator error on a documented type/field underlined the doc block rather than the declaration in the editor squiggle. The originally-plannedBuildContext.locationOfline-arithmetic heuristic overdescription.getContent()was abandoned as unworkable: graphql-java’s processed content cannot distinguish an inline"""text"""block (name on the next line, advance +1) from an own-line block (advance +3) ; both reportmultiLine=truewith zero interior newlines ; and inline blocks are the dominant style indirectives.graphqls;BuildContextalso has no raw SDL to scan. Fixed in the LSP, which holds the raw source and a tree-sitter parse:Diagnostics.signatureRange/descriptionNameRangeresolve the validatorSourceLocationto a tree-sitter point, and when it lands inside adescriptionnode re-anchor the diagnostic range to the enclosing definition’sname(orenum_valuefor enum-value definitions); otherwise the prior column-to-end-of-line fallback is preserved. Exact for every documentation form (single-line, inline block, multi-line block) and location-source-agnostic, so every validator error/warning routed throughvalidatorDiagnosticis re-anchored with noBuildContext/GraphitronSchemaValidatorcall-site changes. AddsDESCRIPTIONtoGraphqlNodeKind. The build-time console / watch-mode formatter (graphitron-core) stays on the graphql-java location (no tree-sitter or raw source there) as a documented lower-priority follow-up. Coverage:ValidatorDiagnosticsTestone test per documentation form (own-line block on a type, inline block on a field, single-line on a type) asserting the range covers the name token, plus a no-description pass-through asserting the column-to-end-of-line fallback; no code-string assertions (diagnostic ranges are the asserted shape). Builds on R147. Independent-session In Review → Done review; full reactor green under-Plocal-db(ValidatorDiagnosticsTest17 tests). -
R388 (
b6b629d): Fix two runtime defects in the discriminated-interface (@table+@discriminate) fetcher when a participant’s FK-target detail table re-declares the discriminator column via a composite FK. Defect 1 (TypeFetcherGenerator): the discriminator column was emitted as a bareDSL.field(DSL.name(col))at all three sites (SELECT projection, LEFT JOIN ON-clause, WHERE filter), making the reference ambiguous and PostgreSQL reject the query once a participant join fired; now qualified to the base table via a two-partDSL.name(baseTableSqlName, col)(renders"base"."col"and preserves theField<Object>the.eq(String)/.in(String…)predicates need, where a table-instance reference would type asField<?>and fail to compile), with the base table’s SQL name threaded throughbuildInterfaceFieldsList/buildCrossTableJoinChain/buildDiscriminatorFilter. Defect 2 (TypeBuilder.extractCrossTableFields): a participant@referencefield whose resolved column already exists on the interface/base table is a contradiction (the column is read directly off the discriminated base table, so a cross-table@referenceis meaningless and the emitted fetcher reads a join-only alias never populated in a non-inline-fragment query); detected once with the catalog in scope, the field is skipped from the cross-table set and a build diagnostic is registered, surfaced through the validator’s existingdrainBuildDiagnostics(the R204/R279/R317 pattern) as anINVALID_SCHEMAauthor error with file:line and a detail-column candidate hint. The spec called for validator-side emission invalidateTableInterfaceType, but that method has no catalog access; the diagnostic-drain is the faithful realisation of the spec’s "resolve once, validator reads rather than recomputes" intent. A participant-only@referencefield stays valid. Fixture:jti_subject+jti_app_account+jti_personjoined-inheritance tables ininit.sql(detail tables re-declare the discriminator via composite FK), corrected-shapeSubject/AppAccount/PersonSDL in the example schema, execution-tier regression tests (GraphQLQueryTest.allSubjects_returnsDiscriminatorPerRow+allSubjects_inlineFragmentDetail_joinsWithoutAmbiguousColumnwith anSQL_LOGqualified-reference assertion), and validation pipeline tests (DiscriminatorReferenceContradictionPipelineTest, both the rejection and the participant-only positive case); no code-string assertions on generated method bodies. First-class discriminated joined-table inheritance (a participant declaring its own detail@table) remains out of scope as R389. Independent-session In Review → Done review; full reactor green under-Plocal-db. -
R380 (
745c0cd): Emit a correlatedEXISTSfor an@reference(path:)filter whose terminal column lives on a joined table, on both filter surfaces. Previously the join path was carried but dropped at projection (input-objectfilter:fields, the motivating utdanningsregisteret bug:STATUS_SELVAKKREDITERENDEonLARESTEDbound againstORGANISASJON, so the generatedConditions.javadid not compile) or never read at all (direct scalarARGUMENT_DEFINITION, where the column resolved against the field’s own table from the outset). Both surfaces share one model + emitter spine and differ only in the classifier locus. Design A (chosen over lifting to a call-siteWhereFilter): theEXISTSis emitted *inside the generated<Type>Conditions.<field>Conditionmethod, so every call site (QueryConditionsGenerator,InlineTableFieldEmitter) stays unchanged and the correlation ties back to whatever alias the caller passes. Model: a new sealedBodyParam.RemoteColumnPredicate(joinPath, inner)wraps an ordinaryColumnPredicate(columns bound to the terminal table) rather than bolting ajoinPathonto the four operator/value-arity arms, mirroring howFkTargetConditionFilterwraps aConditionFilter;name()/list()/nonNull()/extraction()delegate toinner. Emitter (TypeConditionsGenerator): the four local arms collapse into oneColumnPredicatearm viaemitColumnPredicateTerm(cp, alias)+appendGuardedAnd(generated output for local predicates is byte-for-byte unchanged); the remote arm declares one method-local hop alias per FK step, buildsDSL.exists(selectOne().from(terminal).join(…).where(<step-0 correlation back to table>.and(<inner on terminal>))), and the same null / empty-list guard wraps the wholeEXISTS. Classifier (FieldBuilder): Surface 1 stops dropping the parsed path and wraps viaremoteIfReferenceJoin, discriminating plain@reference(Directextraction, terminal column → wrap) from the@nodeIdFK-target lift (NodeIdDecodeKeysextraction, local FK-child columns → stay local); Surface 2 reads@referencebefore the localfindColumn, parses the path, resolves the column against the terminal table, and carries it onScalarArg.ColumnArg.joinPath. Validator mirrors the FK-only precondition at both sites. v1 deferrals (recorded in the implementation commit per the spec):ConditionJoin({condition:}) hops rejected with a typed diagnostic, matching the output-side stub; composite terminals supported by the emitter (unit-tested) but not yet classifier-reachable;@splitQueryrides the same path with no new work. Coverage: pipeline-tierReferenceFilterRemoteColumnPipelineTest(both surfaces lower toRemoteColumnPredicate; single-hop{table:}/{key:}, multi-hop, listIn; nodeId-stays-local discrimination guard;ConditionJoinrejection ; model-level assertions) and execution-tierGraphQLQueryTest(single-hop scalar, two-hop scalar, the input-object filter field reproducing the motivating bug, and absent-arg-returns-all, asserting real rows against the seeded DB).join-with-references.adocrewritten to state the correlated-EXISTSbehavior, multi-hop, null/empty-list semantics, and the FK-hops-only limitation. Independent-session In Review → Done review; full reactor green under-Plocal-db. Carried debt: the new unit-tierTypeConditionsGeneratorTestcases pin theEXISTSbody withcode().toString()contains(…)assertions, matching that file’s pre-existing convention (R375/R79/R50) but contrary to the project’s "no code-string assertions on generated method bodies" principle; the behavior is independently proven at the execution + compilation tiers, so the code-string cases are redundant. Whole-file migration ofTypeConditionsGeneratorTestoff code-string assertions filed as a follow-up Backlog item. Builds on R379; siblings R236 / R282 / R330 and the deferrednodeid-fk-target-arg-join-translation. -
R391 (
c59a11c, Spec4cfb5dc): Add a default-caseGraphitron.newGraphQL()factory to the generated facade.GraphitronFacadeGeneratoremitspublic static GraphQL.Builder newGraphQL()with bodyGraphQL.newGraphQL(buildSchema(customizer → {})), so a zero-extra-wiring consumer writesGraphitron.newGraphQL().build()instead ofGraphQL.newGraphQL(Graphitron.buildSchema(b → {})).build(). Returns a builder (not a built engine), mirroring thenewExecutionInput(…)convention so instrumentation / execution-strategy configuration stays open without a second overload; the body delegates to the facade’s own single-argbuildSchema, keepingbuildSchemathe single schema producer, and in a federation-linked build that path already returns theFederation.transform-wrapped schema, so no federation-specific overload is needed. Call-site sweep: theGraphqlEngineconsumer exemplar plus 23 default-case execution-tier sites converted toGraphitron.newGraphQL().build()(enumerated in the landing commit); the two-argbuildSchema(b → {}, fed → {})federation sites, thefetchEntitiescustomiser, and the raw-SDL spike were deliberately left as-is. Coverage: unit-tierGraphitronFacadeGeneratorTest(structural only, no body-string assertion: method-list now expectsnewGraphQL, plusnewGraphQL_isPublicStaticReturningGraphQLBuilderandnewGraphQL_isPresentExactlyOnceInFederationBuild) and execution-tierFederationBuildSmokeTest.newGraphQLBuildsFederationWrappedEngine(builds vianewGraphQL().build(), asserts_service { sdl }resolves with no errors, the federation-wrap correctness the unit tier cannot reach). Example README updated to showGraphitron.newGraphQL(). Independent-session In Review → Done review; full reactor green under-Plocal-db. -
R99 (
e6df34d): Widen thegraphitron:devLSP scan / walk to sibling modules when the goal runs from inside one sub-module of a multi-module reactor. Maven loads only the started module’s pom there, sogetAllProjects()is a single project and the@service/@condition/@recordclasses in sibling modules were silently invisible: no completions, no hover, no goto-definition, no unknown-class diagnostics, with nothing to grep for. Fix detects the single-project reactor (singleProjectReactor()) and walks up to the nearest ancestor pom whose<modules>lists the current project, then folds each sibling’starget/classesinto the scan side and itssrc/main/javaplus disk-discoveredtarget/generated-sources/*into the walk side, through one sharedsiblingModuleBasedirs()helper consumed by bothresolveClasspathRoots()andresolveCompileSourceRoots()(the codegen reflection loader widens for free through the former). Both halves ride the samecollectExistingDirsexistence filter and dedup, so scan/walk parity (R351/R369) holds by construction: a sibling scanned for completion also has its source root walked. Sibling dirs are resolved by convention in declared<modules>document order (noFiles.list, preserving catalog determinism) and noMavenProjectinstances are built for modules the session never loaded; a genuine standalone module finds no ancestor and is unchanged from pre-R99.DevMojoself-explains the single-module-no-siblings case rather than leaving the silent empty popup. Chose the parent-pom walk-up (Option A) over JAR-classpath scanning (Option B), which would have promoted the R369 unwalked-scanned residue onto the common path and crossed the scanner’s parse-only boundary; dependency-JAR consumers and non-standard sibling<build>dirs stay explicit non-goals. Coverage: unit-tier walk-up tests (document order, nearest-ancestor stop, no-ancestor-empty) and a mojo-tier single-reactor test asserting both that a sibling class lands inexternalReferences()and (load-bearing for R369 parity) that its source root is walked; no code-string assertions on generated bodies. Docs: a "Multi-module projects" subsection ingetting-started.adocand a CLAUDE.md note beside the catalog-jar footgun. Independent-session In Review → Done review; full reactor green under-Plocal-db(AbstractRewriteMojoTest13 tests). Builds on R351/R369. -
R256 (
2d13f72): Absorb the service walker substrate onto typed rejections + multi-arg ctors. R238 shippedServiceMethodCallWalkeras a behavior-preserving translator over an already-resolvedMethodRef.Service, so most of theServiceMethodCallErrortaxonomy it designed was unreachable: every reflection-time failure was produced upstream inServiceCatalogasRejection.structural(…)prose that lost its identity at the LSP boundary (Diagnostics.lspCodeOfreturnsnullforStructural). This item migrates those failures onto typed arms that flow through the existingWalkerResult/Diagnosticsubstrate, keeping reflection at the parse-boundary reader (ServiceCatalog) rather than relocating it into the walker (the design fork the Spec resolved against per "classification belongs at the parse boundary"). Deliverable 1: a newReflectionErrorsub-seal ofAuthorError(graphitron.reflect.) carries the reflection-intrinsic failures shared across the three reflect helpers (reflectServiceMethod/reflectTableMethod/reflectExternalField) —ClassNotLoaded,ReturnTypeMismatch(with aReturnContextSERVICE|TABLE_METHOD discriminant selecting prose),ParameterNamesMissing,AmbiguousMethod— produced at the sharedpickMethod/ class-load / return-type sites so a@tableMethodfailure of the same shape is not forced through a@service-named arm ("one predicate, one home"); the service-binding-specific arms (InstanceHolderUnconstructible,ArgumentParameterMismatch,DtoSourcesUnsupported,UnrecognizedSourcesType) re-land underServiceMethodCallError(graphitron.service-method-call.). Deliverable 3:checkServiceInstanceHolderShaperelaxed toresolveInstanceHolder— resolves any public constructor whose params each bind from a DSLContext slot or a declared context key (legacy(DSLContext)still wins,(DSLContext, ctxArg)now resolves, no-arg admitted),CallShape.InstanceWithDslHoldercarries the orderedctorParams, the walker translates them intoInstance.ctorArgsand raisesMultipleDslContextSlots(CTOR)for a multi-DSL ctor;methods.get(0)replaced bypickMethodso an overload tie producesAmbiguousMethodinstead of silently binding the first declaration-order match. Deliverable 4a:ConflictSite.sitewidened from a bareMethodRefto a sealedSite(Method|Carrier), retiring theContextArgumentClassifier.syntheticServiceMethodRefsentinel that fabricated an emptyMethodRef.Servicejust to satisfy the old field;ResolvedContextArg.sitescarries the widenedSite. Per the Spec’s split-it-out clause, the wire-coercion cast guard stays R261’s (R256 leavesscalarLeafemitting as-is and only guarantees the typed-rejection channel), and deliverable 4b (bean-helper-queueValueShape→ syntheticCallSiteExtraction.InputBeanround-trip) is carved out to R402 (retire-bean-helper-queue-valueshape-roundtrip). Coverage: unit-tierServiceMethodCallWalkerTest(ctor-source translation in order, CTOR-round multi-DSL error),MethodRefCallShapeTest(multi-arg holder ctor + context-only-needs-no-dsl emit),ServiceCatalogTest; pipeline-tierServiceRootFetcherPipelineTest(ReturnTypeMismatch SERVICE+TABLE_METHOD, AmbiguousMethod, multi-arg-ctor resolves without holder rejection) andServiceFieldValidationTest(InstanceHolderUnconstructible) assert on typed arms and stablelspCode`s, no code-string assertions on generated bodies; drift guards `RejectionSeverityCoverageTest(a sample per new permit) andSealedHierarchyDocCoverageTest(typed-rejection.adocparagraph + drift-list per permit) cover all eight new arms. Independent-session In Review → Done review; full reactor green under-Plocal-db(2310 tests; the 3 pre-existing R389JoinedTableInheritancePipelineTestfailures predate this work on an untouched classification path). Builds on R238; pins the boundary with R261; spawns R402. -
R450 (
cf2c34c+ rework1c0126d, Spec7f7c35d/a92bbcb): Fix the split-path hop-0 condition filter binding the same alias as source and target.SplitRowsMethodEmitter.buildWhereConditionemitted a hop-0condition:filter asmethod(firstAlias, firstAlias)(latent since the file’s creation; found in the R435 second-pass review), guaranteeing a javac incompatible-types error for concretely-typed filter parameters and silently self-referential SQL for wildcard ones ; and independent of the alias, the slot-tuple batch grain under-specified the fetch (two parents sharing an FK-slot value got one shared filter verdict). The fix makes grain and topology one decision at one producer:ParentCorrelation.OnConditionJoingeneralizes to the parent-anchor armOnParentJoincarrying only(firstHop, parentTable)with nocondition()accessor (consumers dispatch the hop-0 attach onfirstHop.on():ColumnPairs→ forward join,Predicate→ two-arg condition call);BuildContext.buildParentCorrelation(single producer) lands any hop-0filter()on that arm regardless of itsOn, keepingOnFkSlotsonly for filter-less FK/lifted heads; the batch grain becomes a projection off the arm (parentKeyColumns(): FK source columns / parent PK / routine inputs) read byderiveSplitQuerySource, so parent-PK grain iff parent-anchor topology is structural; the split emitter anchorsparentAliasand binds it as the hop-0 filter source, with a terse classifier-unreachable throw under other arms; record/service split parents with a hop-0 filter reject viaAuthorError.Structuralnaming the escape hatch (previously classified unverified ; Check 2 skips a null originTable). Same-commit consumer audit: three inline emitters re-dispatch onfirstHop.on()(behaviour-identical), split-rows siblings share the parent-anchor path,TypeFetcherGeneratorholds noParentCorrelationswitch. Coverage: pipeline (hop-0 filter → parent-PKsourceKey+OnParentJoin; hop-1 sibling → slot key +OnFkSlotsunchanged; inline hop-0 filter →OnParentJoin), record-parent hop-0-filter Structural rejection fixture,OnParentJoinunit invariants +parentKeyColumnsprojection, and the execution-tier grain proof (twosplit_filter_parentrows sharingtarget_id=1with oppositeincludevalues; split reproduces inline per-parent rows; the concretely-typed condition method also made the pre-fix double-bind fail compile-spec). Rework pass1c0126drepaired the three stale terminal-back-walk javadocs inSplitRowsMethodEmitter(the R449-absorbed housekeeping). Independent-session In Review → Ready → In Review → Done review; full reactor green under-Plocal-dbon both passes. -
R451 (
3ce199b, Spec7ecdf89): Routine writes ;@routineon a Mutation field commits before the follow-up query. A Mutation field carrying@routine(aVOLATILEtable-valued function) plus at least one@referencehop classifies as the new sealed leafMutationField.MutationRoutineWriteField(verbOperation.RoutineWrite) and emits the DML two-step transposed onto the R435 chain: step 1 executes the routine insidedsl.transactionResult(…)(the R429 per-mutation-field boundary; commit on lambda return) capturing only hop 0’s key columns from the routine result, step 2 is a post-commit SELECT anchored on hop 0’s table with the captured keys, remaining hops joined forward, projecting the terminus type ; the routine never appears in step 2’s FROM, so the response always observes committed state. The(start, hops)chain shape extracted into the sharedRoutineChainrecord (one invariant enforcer spanning read and write leaves, exposed via theRoutineChainFieldcapability interface,ServiceFieldprecedent);buildKeysInConditiongeneralizes the DMLbuildPkKeysConditionso both two-step fetchers share the composite-safe key-IN condition.JooqCataloggainedRoutineResolution.NonTableValuedRoutine(a verified probe of the generatedroutinessub-package) so a procedure or scalar/void routine defers to R454 (routine-write-result-shapes, filed ahead of the planSlug repoint) while a genuinely absent name keeps the structural rejection; the single-node Mutation@routineand a condition-joined or filtered hop 0 (no derivable post-commit re-read anchor) likewise land typedDeferred`s. Root-head and multi-routine rules extend to Mutation chains; sakila gained `public.rent_filmand the scalarpublic.rental_count_for_customer(schema 2.8 → 2.9). Coverage:ClassifiedCorpusroutine-mutation-writeentry,GraphitronSchemaBuilderTestR451 block,RoutineMutationWritePipelineTesttwo-step fingerprint pin (sanctioned call-site form), and execution-tierrentFilm_*(commit observed by independent read; failing routine rolls back with nothing committed).@routinedirective reference gained "Writes on Mutation". Independent-session In Review → Done review; full reactor green under-Plocal-db. Builds on R449/R435/R429; spawns R454. -
R429 (
f68666c/776f0d9/3a0f0dd/da38754/5bb881f/c2664aa+ rework7b86287/09ebe0d/171d468/ce5149d/11cf1e3): Graphitron owns the connection lifecycle ; application runtime, operation-typed transactions, and database-mounted session identity. An emitted application-scopedGraphitronRuntimeowns the consumer’sDataSource; every operation pins exactly one connection (safe because batch loaders run SQL synchronously on the dispatch thread, tripwired atRowsMethodCallTest); the caller’s claims travel as an opaqueStringto a consumer-owned database connect hook at acquisition with a paired disconnect hook at release (fail-closed connect, evict on unmount failure, both hooks structurally outside any transaction: acquire normalizes autocommit before connect, release settles any open transaction before disconnect). Queries run in autocommit (blanket read-only enforcement split to R460); each mutation field commits or rolls back independently through the emittedGraphitronTransactionProvider(commit-policy axis:ROLLBACK_ONLYis R428’s rollback-everything dev mode);@deferstays off on the owned path (follow-on R469).<sessionState>emits the hook from config: function-hook callables with optional OUT-handle threading and the<stateSurvivesTransactions>survival opt-in (undeclared pairs re-fire per mutation-field settle through the provider’s opaque settle callback, so a settle can never leave stale or reverted identity; the read path is untaxed), or the Postgres<variables>sugar emitting both halves from one carrier (survives settles structurally; convention-fence warning with@servicepresent; Oracle/RAS execution coverage is R468). Per-request entry isGraphitron.newOwnedExecutionInput(claims, …)beside the R190 escape hatch (kept, with a one-time caller-owns-everything wiring notice); the tenant-keyedTenantConnectionscarrier lands the acquisition seam R45 consumes. Docs:runtime-extension-points.adocrewritten for both paths, RLS-assumed principle, integrity gradient (enforced/convention/cryptographic fence), MP-JWT adapter recipe, hook state contract (session-scoped, never transactional). Sakila app adapter migrated as first client of the owned path. Independent-session In Review → Ready → In Review → Done review; full reactor green under-Plocal-dbon both passes. Builds on R190; feeds R45/R428; spawns R460, R468, R469. -
R428 (
5488bc4/ae91f86/29420c8/ab56510+ rework43de546/3758ec3, Specfd33e86): MCPexecutetool runs a GraphQL query/mutation against the generated resolvers in-process in thegraphitron:devJVM, closing the authoring loop (validate-error → compile-error → real result) with no app server. The load-bearing move is codegen, not a runtime seam: graphitron emitsGraphitronDevExecutorinto the output package, compiled in the same R410 pass, exposing onepublic static String execute(Connection, String dialect, String query, Map variables, String claims, Map contextArgs)whose signature is JDK-only, so the dev-loop host reflects exactly one method and no jOOQ / graphql-java type crosses the host↔generated classloader boundary; everything schema-varying (thenewOwnedExecutionInputsignature, the typed contextArgument binding, whether<sessionState>is configured) is absorbed at generation time. Inside, the executor wraps the host’s single dev connection in a one-connectionDataSource, constructs the R429GraphitronRuntimewith the requested dialect, and runs under theROLLBACK_ONLYcommit policy so the dev loop exercises the same acquisition/hook/transaction path a real app does while never persisting a write. Host half (DevQueryExecutorin graphitron-mcp): a fresh platform-parentedURLClassLoaderper call overtarget/graphitron-classesfirst (R410 shadowing invariant) plus the consumer classpath, JDBC driver discovered viaServiceLoaderon the project loader (DriverManager bypassed), TCCL pinned to the generated world for the call, executor-side failures (connect-hook rejection, fail-loud missing claims) surfaced verbatim. Config is a<devDatabase>block (url/user/password/dialect/claims/allowClaimsOverride) with env-wins overrides (GRAPHITRON_DEV_DB_*,GRAPHITRON_DEV_CLAIMS); explicit enumerated dialect (POSTGRES/ORACLE, never defaulted); absent url disables the tool quietly (RAG-style degrade);<sessionState>schemas fail loud on missing claims namingGRAPHITRON_DEV_CLAIMS; per-call claims override rejected unless opted in. R429 contract change flagged and reviewed: theROLLBACK_ONLYarms of the generated transaction provider became a deferred observe-then-discard topology (operation transaction opens once and defers across field settles, each field boundary a savepoint, read-backs observe the writes,PinnedConnection.releasediscards everything) because the shipped R449/R451 DML two-step reads back committed state; the one stated fidelity limit (no mid-operationafterSettlere-fire under this policy) is documented in the provider/executor javadocs and pinned by a provider unit test. Federation_entitiesexecution and opt-in commit are named follow-ons. Coverage:GraphitronDevExecutorGeneratorTest+…PipelineTest(structural signature/gate/helper only, no body-string assertions),GraphitronTransactionProviderGeneratorTest(compiled-and-driven deferred topology),DevQueryExecutorTest/ExecuteToolTest/DevMojoTest/GraphitronMcpServerTest, and execution-tierDevExecuteExecutionTest(real Postgres: executor JSON byte-equal to a direct in-app execution, variables binding, observable-write + no-trace mutation, field independence, fail-loud/malformed claims). Independent-session In Review → Ready → In Review → Done review; both flagged findings (build-red on a missingmojo-configuration.adocrow for<devDatabase>; code-string assertions on generated bodies) fixed in the rework pass, which also corrected the spec’s false "driver is on the compile classpath" assumption by resolvingResolutionScope.TEST(the JDBC driver lives at runtime/test scope for plain/Quarkus apps). Full reactor green under-Plocal-db(independently re-verified, unpiped exit code). Note: the sakila-example POM’s new plugin-level<sessionState>(for CLImvn graphitron:dev) is inherited by the federated/multischema generate executions via Maven config merge, so those fixtures now additionally emit aGraphitronSessionHook; harmless (build green) but a behavior change the landing commit described as "unaffected". Builds on R410/R429/R118.