Rewrite Changelog
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.
-
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.