This is the rendered roadmap. Plans are authored as markdown in roadmap/; this view derives from the per-item front-matter and the plan bodies. For the model taxonomy, see Code Generation Triggers. For design principles, see Graphitron Development Principles. For per-leaf classifier coverage, see Inference-axis coverage report. Or jump to the by-theme view or the changelog. Back to home.

Active

ID Item Status Updated Plan

R222

Dimensional model pivot: slots over cross-product permits

Spec

2026-07-04
created 2026-05-21

plan

R333

The Graphitron data model

Spec

2026-07-05
created 2026-06-18

plan

R458

Per-participant explicit join paths on multi-table interface/union child fields

Ready

2026-07-09

plan

R314

Dissolve the re-fetch (reentry) leaf fields: emit reentry by switching on the model
blocked by: coordinate-lowers-to-datafetcher-queryparts, decompose-sourcekey, collapse-split-and-record-table-leaves

Spec

2026-07-04
created 2026-06-15

plan

R335

Fold input/scalar/enum classification into the single classify-and-emit walk

Spec

2026-06-19

plan

R308

Model carrier arrival on the @service payload seat: one coherent list-payload shape verdict

In Review

2026-07-12
created 2026-06-14

plan

R381

LSP-guided @reference path authoring

Spec

2026-06-25

plan

R45

Operation-divined tenant routing: tenant-column bindings select the per-tenant DataSource

Spec

2026-07-03

plan

R273

Source NodeId metadata from @node + catalog PK (inferred from implements Node), and settle wrong-type/malformed mismatch semantics, retiring the legacy __NODE bare-ID arm

Spec

2026-06-02

plan

R347

Consolidate graphitron-lsp navigation, dispatch, and result-building

In Progress

2026-07-01
created 2026-06-19

plan

R13

Faceted search on @asConnection

Ready

2026-06-26

plan

R92

Surface database CHECK constraints as Jakarta validation rules

Spec

plan

R180

Centralize ResultType column-read emission for @record parents

Spec

2026-05-19

plan

R463

Consume R279’s ancestor-cardinality rider: fold true arrival and populate Source.OnlyChild

Spec

2026-07-10

plan

R242

DML payload positional input/output alignment

Spec

2026-06-15
created 2026-05-26

plan

R115

Enumerate the capabilities graphitron delivers

Spec

plan

R109

How-to recipe and Sakila fixture for grouped collections via Field<Result<R>> @externalField + multiset

Spec

plan

R212

IntelliJ plugin wrapping graphitron:dev LSP

Spec

2026-05-21

plan

R112

Operation-driven test corpus, capability catalog, and runtime trace
blocked by: capability-catalog

Spec

plan

Backlog

Architecture

  • R234 Support jOOQ embedded and UDT records as non-table input backings: R222 collapsed the legacy JooqRecordInputType arm by rejecting any non-TableRecord jOOQ Record subclass at classification with Rejection.AuthorError("backing class %s is a jOOQ Record but not a TableRecord; supported non-table backings are Java record or POJO"). That stance is correct for today’s graphitron-fixtures-codegen and Sakila surfaces (no real schema currently binds a non-table jOOQ Record as an input), but it bakes in a rejection that will trip the moment a consumer wants to use a jOOQ embeddable record (jOOQ 3.20+ feature for grouping related columns into a typed structure) or a UDT record (PostgreSQL composite type) as an input. Both are legitimate jOOQ-side carriers; both have stable Java accessors but no TableRef of their own. This item reintroduces the backing-class arm(s) those cases need — likely EmbeddableRecord(fqClassName, embeddable: EmbeddableRef) and UDTRecord(fqClassName, udt: UDTRef) rather than a generic JooqRecord catch-all, so each arm carries the structural metadata its downstream consumers actually want. Scope: extend BackingClass with the new arm(s), wire the visitor’s runtime-shape classification to detect them, decide whether they participate in classifiedFields (UDT-typed inputs may project onto a single column whose value is the UDT instance, which is a different code path than table-bound inputs), and add fixtures. Out of scope for R222 because no consumer needs it yet; lands once a real fixture or user case surfaces. (updated 2026-05-23)

  • R411 Wire-coercion cast guard for @condition and @externalField (R261 Slice 2): R261 Slice 1 landed the wire-coercion cast guard for the three @service sites (A: input-bean scalar field, B: @service scalar arg, E: input-bean enum field) plus the shared classify-time predicate (WireCoercionResolver), the WireCoercionError sealed family, and the enum-constant parity home (EnumMappingResolver.checkEnumConstants). This item is R261’s deferred Slice 2: the same defect on the two non-@service arg-classification sites the Slice 1 spec named but left out: (updated 2026-07-01) (blocked by dimensional-model-pivot)

  • R469 Enable @defer/incremental delivery on the owned-connection path: R429’s owned-connection path releases the pinned connection at operation completion, so a deferred fetcher running after the initial result would use a closed connection. The V0 stance is therefore that incremental delivery stays off: the owned factory never opts in, and GraphitronConnectionInstrumentation.beginExecuteOperation rejects an execution with incremental support enabled outright (pinned by ConnectionLifecycleExecutionTest). Enabling @defer/@stream under owned connections must own the connection-lifetime story: when release happens relative to deferred delivery, how session identity stays mounted (or remounts) for late fetchers, and how that composes with the per-settle re-fire fallback and the tenant-keyed carrier. Named as a follow-on in R429’s @defer section; this item is its tracking home now that the R429 spec is deleted. (updated 2026-07-10)

  • R468 Oracle/RAS execution-tier coverage for session identity hooks: R429 shipped the session identity hooks (connect/disconnect from <sessionState>, handle threading, the per-settle re-fire fallback for hooks without <stateSurvivesTransactions>) with execution-tier proof on Postgres only; Oracle stays unit-tier because the build has no Oracle container. The Oracle worked example is the load-bearing one for Sikt’s kernel API (definer-rights package, VPD institution context, RAS CREATE_SESSION/ATTACH_SESSION with the session id as the OUT handle, detach/destroy by handle on disconnect), so the two-hook contract, handle capture and rebinding, the outside-any-transaction invariant, and the re-fire fallback should each be proven against a real Oracle database once a container (or an external test target, like the test.db.url seam the Postgres tiers already use) is available. Named as a follow-on in R429’s slice 3; this item is its tracking home now that the R429 spec is deleted. (updated 2026-07-10)

  • R171 Fold InputType and TableInputType under sealed parent InputLikeType: GraphitronType today permits InputType (with four leaves: PojoInputType, JavaRecordInputType, JooqRecordInputType, JooqTableRecordInputType) and TableInputType as siblings. Any capability uniformly true of "things that come in as SDL input" (R94’s HasInputRecordShape, R98’s ConstraintSet-attachment slot, any future input-side carrier) must be declared on five places instead of one, and a sixth input-like variant added to GraphitronType.permits will not get a compile-time miss for the capability. Fold the two siblings under a sealed interface InputLikeType extends GraphitronType permits InputType, TableInputType, and relocate input-side capabilities onto that root. Cleanup item surfaced by R94’s capability-declaration site; deferred from R94 to keep that item narrow. Not blocking: capability interfaces declared on five sites work today, the fold tightens the sealed root so the compiler enforces the invariant. (updated 2026-05-17)

  • R337 Input-side nesting-projection classification (NestingType mirror): Deferred in favor of R97 (consumer-derived-input-tables). This item proposed to fix the null-backed PojoInputType mislabel for nested grouping inputs by adding a new per-type GraphitronType variant (an input mirror of the output NestingType). R97’s fact-model framing (which absorbed R327, field-relative-input-classification, on 2026-06-22) claims the same artifact from the opposite direction and supersedes that mechanism, so R337 is parked as a redirect rather than spec’d. Revive it only for the narrow residual below, and only if R97 lands without covering it. (updated 2026-06-22, created 2026-06-19) (blocked by consumer-derived-input-tables)

  • R397 Let bare-entity query fields host @error so decode and other client errors route through handlers: @error is on OBJECT: handlers are declared on a payload object that carries an errors field, and the error channel binds to fields whose return type is such a payload. This already works for query fields whose return shape is payload-like: WithErrorChannel is implemented by QueryServiceTableField, QueryServiceRecordField, QueryServicePolymorphicField, and QueryTableMethodTableField (WithErrorChannel.java:13 names "root + child services, root + child @tableMethod fields"), and CatalogBuilder resolves errorChannelName(f.errorChannel()) for them. What has no error channel is a plain bare-entity fetch field (soknader: [Soknad!]): the sealed QueryField interface does not extends WithErrorChannel (QueryField.java:25, unlike MutationField.java:18), the table-fetch variants do not implement it, and their fetcher wraps work in a no-channel catch arm (TypeFetcherGenerator redactCatchArm). So a client-facing error raised while fetching a bare-entity field cannot be mapped to a typed @error payload; the consumer’s only lever is the no-channel disposition. (updated 2026-06-29)

  • R103 Lift jOOQ column defaults onto input fields connected to that column: When a GraphQL input field is wired (via @field(name:) or implicit name match) to a jOOQ-generated column whose DataType carries a defaulted() expression, surface that default in the schema so clients can see it and so omitted values get a typed, server-known default rather than silently relying on the database. The current generator path already emits DSL.defaultValue(dataType) when an input key is absent at insert/update time (TypeFetcherGenerator.java:1456, :1496, :1508, :1769), so the runtime story is correct — the gap is purely on the contract side: the SDL says nothing about which input fields have a database-supplied default, and clients that introspect the schema have to read the migrations to find out.

  • R231 Emit text-mapped-enum fields as the GraphQL enum type, not String: When an SDL field is declared with an enum return type whose values use @field(name:) to bind to a varchar column (e.g. textRating: TextRating with enum TextRating { PG_13 @field(name: "PG-13") …​ }), graphitron’s field-emit lowers the GraphQL field type to String in the generated FieldDefinition (see FilmType.java:36 for textRating). The fetcher returns the raw column string, and graphql-java’s Coercing layer is never engaged for that field — clients see the runtime form ("PG-13") instead of the SDL identifier (PG_13). (updated 2026-05-22)

  • R57 FK-target argument @nodeId, JOIN-with-translation emission: R40 shipped the simple direct-FK case for argument-level FK-target @nodeId: when the FK source columns positionally match the target NodeType’s keyColumns, projectFilters emits BodyParam.In / Eq / RowIn / RowEq against joinPath[0].sourceColumns() directly, no JOIN required.

  • R72 Slim ServiceCatalog down to a lookup primitive: ServiceCatalog.java (~700 lines) is named like a lookup helper but carries three resolver-sized routines on top of the lookup: reflectServiceMethod (~170 lines), reflectTableMethod (~90 lines), and reflectExternalField (~50 lines). Each owns its own Class.forName + getDeclaredMethods scaffolding, its own argument-binding policy (override targets, typo guards, SOURCES batching hints), its own expected-return-type rule, and its own rejection wording. The caller surface (ServiceDirectiveResolver, TableMethodDirectiveResolver, ExternalFieldDirectiveResolver, ConditionResolver, BuildContext) is the natural home for those policy decisions; the catalog should expose lookup-shaped operations only.

  • R460 Targeted read-only enforcement for query paths graphitron does not control (@routine, @service): R429 originally ran every query operation inside a read-only transaction (SET TRANSACTION READ ONLY, DB-enforced) so a query "literally cannot write". That guarantee costs a per-request round trip (the demarcation, plus the trailing commit), which on a high-latency database measured ~15ms per round trip on one Sikt subgraph. The cost/benefit does not hold up for the common path: graphitron’s generated query fetchers only ever emit SELECT, so read-only enforcement is guarding against a write those fetchers cannot produce. The only query surfaces where graphitron genuinely does not know whether a write can happen are the two escape hatches to code it did not generate: @routine (an arbitrary database routine, which may be VOLATILE / may write) and @service (consumer Java that receives the pinned-connection DSLContext and may issue any SQL). R429 therefore drops blanket read-only enforcement; this item re-introduces it narrowly, only where the SQL is uncontrolled, and/or offers zero-per-request-cost realizations for consumers who still want a broad read-only guarantee. (updated 2026-07-09)

  • R319 Warn on pruned unreachable output types instead of dropping them silently: R279 slice 6 made classification reachability-pruned: an output composite (object / interface / union) that the field-first walk never reaches is no longer classified, gets no generated file, and the prune is observable. That is the right behavior, but it is currently silent: an author who writes a type no field returns gets no signal that their type was dropped, which reads as a mysteriously missing resolver. The intent is a warning, not an error: unreachable types are allowed and pruned, and a healthy reachable schema must still build, so this must never block. Emit a build warning naming each pruned unreachable output type (and ideally why it is unreachable), so dead schema surfaces without failing an otherwise sound build. Small, additive, and orthogonal to R317’s classification rework; it rides best on top of the single-pass walk R317 lands (R317 slice 3 shifts unreachable output types from classified to pruned; this item adds the warning). (updated 2026-06-17)

  • R97 Deprecate @table on input types; consumer-derived tables + argMapping grouping: The @table directive on input types declares "this input maps to columns of table X". The classifier consumes it to produce GraphitronType.TableInputType (TypeBuilder.buildTableInputType at TypeBuilder.java:686-718), and downstream MutationInputResolver, EnumMappingResolver.buildLookupBindings, FieldBuilder (line ~697), and GraphitronSchemaValidator.validateTableInputType all switch on that variant. The directive is the structural signal that drives DML emit, @lookupKey resolution, and condition-input column binding.

  • R239 Lift ColumnField.parentTable from emitter parameter to record component: Surfaced by R237 Phase 2 as a (b-cheap) structural-lift candidate. The classifier produces a ChildField.ColumnField only on a table-backed parent, but the parent table itself is currently threaded into TypeFetcherGenerator.generateTypeSpec as a parameter rather than carried on the ColumnField record. The switch arm at TypeFetcherGenerator.java:319 reads parentTable from the parameter and throws IllegalStateException if null, treating a structurally-precluded reachability as a defensive guard. (updated 2026-06-26, created 2026-05-25)

  • R192 Mojo-configured custom Bean Validation factory: Originally drafted as part of R45 (tenant-routing-and-execution-input.md) and inherited by R190 (single-tenant-execution-input-factory.md); carved out because the validator-override mechanism is independent of those items' surface narrowing. The generated GraphitronContext impl’s getValidator(env) returns Validation.buildDefaultValidatorFactory().getValidator(); consumers who need a custom Validator (custom ConstraintValidator implementations, alternative providers, CDI integration) have no seam today and would have to reach for the legacy GraphitronContext interface that R190 seals. The proposed shape is a new Mojo element naming a consumer-supplied factory class whose instance graphitron calls per request: this pushes against R45’s "extension points that don’t pay for the openness" critique one level down, since the override hook is itself a per-request consumer-implemented surface. The Spec author must justify whether the override is per-request (functional interface, (DataFetchingEnvironment) → Validator) or per-build (configured class graphitron instantiates once and calls .getValidator() on), and what the migration story is for consumers currently overriding GraphitronContext.getValidator(env) in the legacy generator. Depends on R190 landing first so the sealed-context method set is the baseline this item widens. (updated 2026-05-20)

  • R323 Multi-parent NestingField sharing: BatchKey leaves: Follow-up to R23 (which shipped the TableField arm of multi-parent NestingField sharing). GraphitronSchemaValidator.compareNestedFieldsShape still rejects a plain-object nested type shared across multiple @table parents when any shared leaf is a BatchKey carrier — SplitTableField, SplitLookupTableField, RecordTableField, RecordLookupTableField, or a Record*MethodField — with the "not yet supported across multiple parents" deferral. Unlike the projected TableField leaf R23 admitted, these variants carry per-field DataLoader registration and per-parent rows-method generation keyed off the outer parent context, so reconciling them across a shared nested type is real work, not just a validator gate: each variant’s LoaderRegistration / SourceKey resolution and rows-method signature would need to agree (or be made per-parent) across the sharing parents. Each variant has its own considerations; the spec should decide whether to land them together or per-variant. (updated 2026-06-17)

  • R46 Multi-tenant fan-out: run one field across many tenants and union the results: Two production patterns need the same shape: (updated 2026-07-03) (blocked by tenant-routing-and-execution-input)

  • R123 Parent-context-aware schema coordinates for per-directive Behavior policy: R119 keyed the LSP’s directive vocabulary on GraphQL-spec schema coordinates: Directive, DirectiveArg, InputType, InputField. The granularity is correct per the GraphQL spec, but it under-specifies one axis the LSP needs.

  • R66 Widen string-carrier intermediates onto Rejection (R58 follow-up): R58 lifted the direct candidate-hint producers onto typed [Rejection.AuthorError.UnknownName](../graphitron/src/main/java/no/sikt/graphitron/rewrite/model/Rejection.java) factories. Five intermediate carriers still flatten the typed shape into prose before it reaches a Rejection consumer, blocking five candidate-hint producers from reaching the typed surface their factories (unknownForeignKey, unknownTypeName, unknownEnumConstant, unknownNodeIdKeyColumn, unknownColumn) already exist for. R58 Phase D shipped the factories; this plan adds the carrier widenings so the typed values reach consumers.

  • R11 DSLContext on @condition / @tableMethod methods: Lift the reflectTableMethod gate. Requires ArgCallEmitter to walk params() instead of callParams() so the injected DSLContext lands at its declaration-index slot.

  • R71 @batchKeyLifter Record return-type symmetry: [R61 (emit-record1-keys-instead-of-row1.md)](emit-record1-keys-instead-of-row1.md) added RecordN<…​> source-shape support alongside the pre-existing RowN<…​> shape on the @service classifier path: developers freely choose either at the source declaration, and variant identity tracks shapeRowKeyed / MappedRowKeyed carry RowN keys; RecordKeyed / MappedRecordKeyed carry RecordN. The only consumer-supplied surface left without that symmetry is @batchKeyLifter, where BatchKeyLifterDirectiveResolver still pins the lifter method’s return type to org.jooq.Row1..Row22 (BatchKeyLifterDirectiveResolver.java:266-273); a Record1..Record22 return is rejected today. This item brings the lifter API to the same Row-or-Record symmetry the source-shape path already has.

  • R98 Multi-source input validation: SDL directives + DB CHECK + Jakarta on a unified rendered schema: R94 emits an internal Java record per SDL input type. R92 phase 3 attaches programmatic Jakarta ConstraintMapping entries to those records derived from PostgreSQL CHECK constraints. R12 §5’s pre-execution validator step runs against each input at the fetcher boundary. Three pipes today; three different sources of truth for "what does this input need to look like to be valid"; only one of them (DB CHECK) is currently surfaced to consumers anywhere outside of the runtime violation report. (blocked by catalog-check-constraint-validation)

  • R193 Sealed UnresolvedParam classification for @service parameter rejection arms: The diagnostic-arm decision inside ServiceCatalog.reflectServiceMethod (ServiceCatalog.java:258-329, the sourcesShape.isEmpty() block) is a chain of predicates over the unresolved Java parameter: classifySourcesType().isEmpty(), then pName == null, then parentPkColumns.isEmpty() && looksLikeSourcesShape(…​), then dtoSourcesRejectionReason(…​) != null, then the generic "unrecognized sources type" fall-through. Two recent bug items (R185 root, R187 nested) each adjusted the precedence in different directions ; R185 narrows the SOURCES-batch arm so List<XRecord> at root falls through to the arg-mismatch diagnostic; R187 drops the parentPkColumns.isEmpty() gate so the arg-mismatch arm fires at nested coordinates whenever the parameter isn’t SOURCES-adjacent. Both fixes are correct, both ship as small surgical diffs, but the cumulative shape is a fan-out of overlapping predicates with no single record that says which classification the parameter actually fell into. The principles-architect review on R187 flagged this directly: precedence is a property of the classifier, not the diagnostic emitter, and asking it in two places invites future bugs whenever the predicate set grows again. (updated 2026-05-20)

  • R240 Type-token threading on MethodRef.StaticOnly + ReturnTypeRef.TableBoundReturnType: Surfaced by R237 Phase 2 as a (b-relational) structural-lift candidate. ServiceCatalog.reflectTableMethod rejects developer methods whose return type is wider than the generated jOOQ table class via a strict ClassName.equals comparison, and TypeFetcherGenerator.buildQueryTableMethodFetcher (generators/TypeFetcherGenerator.java:1035, :1114) declares <SpecificTable> table = Method.x(…​) with no cast and feeds the local directly into <SpecificTable>Type.$fields(…​). The contract is a relationship: the field’s table token equals the method’s return token at runtime, but neither MethodRef.StaticOnly nor ReturnTypeRef.TableBoundReturnType carries this relationship structurally. (updated 2026-06-26, created 2026-05-25)

  • R172 Audit: forbid service-side references to <outputPackage>.inputs.*: R94 emits a graphitron-internal Java class per SDL input type under <outputPackage>.inputs.<InputName>. The class is a Jakarta-validation target: the fetcher boundary calls <InputName>.fromMap(env.getArgument(…​)), hands the result to validator.validate(…​), and discards it. Service code (under the consumer’s package, never under <outputPackage>.inputs) must not reference these classes; doing so re-creates the service-side-graphitron-coupling R150’s design rules out. (updated 2026-05-17)

  • R122 Compound mutations: parent entity row + child normalised rows in one INSERT: A common entity-storage pattern is one parent row in an entity table plus N rows in one or more normalised child tables (typed-attributes, many-to-many association rows, etc.) keyed off the parent’s PK. Today graphitron’s @mutation(typeName: INSERT) admits exactly one DML target table per mutation; consumers wanting "insert one entity + its normalised children" have to author a @service mutation that orchestrates the inserts in Java, even when the relationships are entirely declarative from the SDL/jOOQ catalog perspective. (updated 2026-05-23)

  • R427 Relevance-ranked free-text search: > Origin: [issue #512](https://github.com/sikt-no/graphitron/issues/512) > ("annotering av flere felter for å auto-generere søkeindeks"). This item is a > Backlog thinking-capture, not a spec. It records the framing and the > insights uncovered in early design discussion so they are not relitigated > later. It is deliberately not Spec-ready; the open forks below must be closed > first. (updated 2026-07-02)

  • R25 Rebalance test pyramid: Shift new test investment from per-variant structural tests toward SDL-to-classification-to-emission pipeline tests keyed off graphitron-fixtures.

  • R174 graphitron-javapoet: emit records, sealed/permits, package-info.java: graphitron-javapoet is forked from Square’s JavaPoet at a point that predates Java records (Java 14+) and sealed types (Java 17+). The emit framework supports four TypeSpec.Kind values: CLASS, INTERFACE, ENUM, ANNOTATION. Records, sealed/permits clauses, and package-info.java files cannot be generated through the framework today. The rewrite uses graphitron-javapoet exclusively for code emit, so any emitter that wants to produce these shapes hits a wall. (updated 2026-05-17)

  • R7 Decompose TypeFetcherGenerator: TypeFetcherGenerator.java is 1 646 lines, one public entry point (generate(GraphitronSchema)), and ~30 private methods that implement per-field-variant emitters plus shared helpers. It is the counterpart to the now-shipped FieldBuilder decomposition (R6, see [changelog.md](changelog.md)): a central generator that has accumulated coverage faster than its file shape can absorb.

  • R145 Cardinality safety story for UPSERT under the multiRow: regime: R144 inverts the cardinality-safety polarity on DELETE and UPDATE (default treats every input field as a WHERE filter; PK coverage required; multiRow: true on @mutation is the opt-out). UPSERT is carved out at R144’s classify-time rejection because its semantics differ: INSERT …​ ON CONFLICT (cols) DO UPDATE SET …​ requires the conflict-target columns to form a unique constraint by definition, and one input row matches at most one existing row. The multiRow: knob does not apply the same way. This item designs the UPSERT-specific safety story, lifts R144’s classify-time rejection, and restores UPSERT-generation. Existing UPSERT fixtures in sakila-example and GraphitronSchemaBuilderTest migrate as part of this work.

  • R207 Audit design-doc claims for implementation conformance: R205 surfaced a five-layer survival pattern where a documented design claim (docs/argument-resolution.adoc’s truth table at `:262-275 saying plain inputs and @table inputs share the same implicit-predicate behaviour) diverged from the implementation (FieldBuilder.java:1349 passing null for implicitBodyParams on plain inputs) and survived because no enforcing test asserted the symmetry. The same shape — design doc says X, code does Y, no test pinning X — plausibly exists elsewhere in the rewrite-internal docs (argument-resolution.adoc, typed-rejection.adoc, development-principles.adoc, per-resolver javadocs). (updated 2026-05-21)

  • R218 Carry inference provenance on ParamSource.Arg so resolved bindings audit cleanly: R214’s ServiceCatalog.inferBindingsByType mutates argByJavaName silently between the override-typo check and the per-parameter loop. The resulting ParamSource.Arg(extraction, path) is structurally identical regardless of whether the binding came from an explicit argMapping, a same-name identity match, the arity-unique inference branch, or the type-unique inference branch. The resolved-coordinate report and any future LSP "where did this binding come from?" surface can’t tell them apart. The principles-architect review (round 1, finding 4) flagged this as a "load-bearing invariant doesn’t have an emit-time witness" gap, citing the typed-rejection / auditable-resolution narrative the project rests on. (updated 2026-05-21)

  • R220 Consolidate looksLikeSourcesShape, couldBeSourcesShape, and classifySourcesType into one predicate: ServiceCatalog now has three closely-related predicates over the same Java parameter shapes, each subtly different: looksLikeSourcesShape (Row<N> / Record<N> lists only, used by the root-coordinate diagnostic), couldBeSourcesShape (R214 addition; adds TableRecord to the above, used by the inference gate to exclude SOURCES-shape params from candidate binding), and classifySourcesType (gated by parentPkColumns.isEmpty() and produces a typed SourcesShape result). The principles-architect review (round 1, finding 5) flagged this as the "same predicate evaluated by multiple consumers" smell — the resolver is under-specified, and the three predicates have already drifted apart in subtle ways. (updated 2026-05-21)

  • R117 Graphitron knowledge base programme: DuckDB as queryable model: This item is a programme, not a single deliverable. It frames the DuckDB store graphitron emits at build time as a queryable model of everything graphitron knows about itself: the SDL it parses, the classifications it produces, the code it generates, the runtime it observes, the documentation it ships, the roadmap that drives it. R104 introduced the store as a coverage scratchpad. R112 extends it with operations, capabilities, and runtime trace. The programme this item defines is the deliberate continuation: keep adding dimensions, keep them naturally keyed, keep the store a projection (rebuilt on every build, never a competing source of truth), and grow toward a knowledge graph queryable end-to-end. The consumer surface is R118 (the graphitron MCP server), but the value lands first inside the build itself: each dimension absorbed makes the next coverage view, doc render, or static check materially cheaper to write.

  • R304 Reify @error PayloadAccessor errors fetcher into a named method: R303 reified every datafetcher onto a named <Type>Fetchers method except one: the @error-type errors field on the Transport.PayloadAccessor arm, which still registers graphql-java’s PropertyDataFetcher.fetching(name) (a runtime reflective property read off the parent payload). (updated 2026-06-14)

  • R219 Unify arity-unique and type-unique inference under a single JavaTypeKey-counted rule: R214’s inferBindingsByType shipped with two sibling rules (arity-unique and type-unique) sequenced as a "fallback ladder": arity-unique returns early when applicable; type-unique handles the residual case. The principles-architect review (round 1, finding 2) flagged this as a discontinuity: a working schema (input: SomeInput) → (SomeInput payload) binds via arity-unique today; the moment an SDL author adds a second argument of an unrelated type, the same payload now needs the type-unique branch, which by construction can’t see named-input-object slots (mapToJavaTypeName returns null for them and they’re dropped from slotsByType). The binding silently disappears. The user’s stated rule was "one and only one possible mapping" — a second argument of a different type doesn’t introduce a second mapping for the existing pair, it just adds a sibling. (updated 2026-05-21)

  • R221 Validator walks PlainInputArg.fields() for UnboundField rejection: R215’s validator-side check on InputField.UnboundField (per the spec’s §Validator rules: condition.isPresent() && !condition.get().override() → reject at the directive’s source location) only walks GraphitronType.TableInputType.inputFields() via GraphitronSchemaValidator.validateTableInputType + validateInputFieldRecursive. Truly plain input types (GraphitronType.InputType permits — JavaRecordInputType, PojoInputType, JooqRecordInputType, JooqTableRecordInputType) carry no classified InputField records on the type; their fields classify at consumer time on ArgumentRef.InputTypeArg.PlainInputArg.fields(). The validator’s whole-schema walk has no view into PlainInputArg, so plain-input UnboundField + @condition(override:false) shapes escape the validator-mirrors-classifier rule. R215’s acceptance test #5 (r215_validatorRejectsOverrideFalseOnNonBindingField) was written against a @table input (which routes through the existing walker) so the test passes, but the literal spec phrasing "non-binding plain input field" is structurally unreachable today. (updated 2026-05-21)

Cleanup

  • R263 Add a typeName-first decode-helper entry point so resolveDecodeHelperForTable is not a misuse trap: BuildContext.resolveDecodeHelperForTable (BuildContext.java:2111-2136) resolves the decode<TypeName> suffix from findGraphQLTypeForTable(sqlTableName) (singular, :1992, :2118) and consults its fallbackTypeNameOrTypeId argument only on the empty branch (:2133). A caller that holds an authoritative @nodeId(typeName:) and passes it expecting it to drive the suffix is silently ignored whenever a @table type backs the table; when several object types share that table the method yields decode<firstTypeForTable>, not decode<TypeName>. The argument reads as suffix-bearing but is not, so the misuse compiles and the wrong helper only surfaces in the multi-type-per-table configuration, an awkward case to land in a test. (updated 2026-05-30)

  • R360 Retire the @enum directive; infer enum Java backing from producers: The @enum(enumReference:) directive links an SDL enum to a Java enum class, but that backing is inferable authoritatively from the use site: an enum-typed coordinate resolves a producer (a column javaType, a @service signature type, an accessor return type) whose Java type is the backing. This is the same producer-reflection that retired @record. So an authored @enum value adds no information graphitron cannot derive and can only contradict the inferred truth, a pure misconfiguration surface. Retire it the way @record / @notGenerated / @multitableReference are retired: keep the declaration so the parser does not choke, reject any application at classify time with a migration message, and derive the enum’s backing (the EnumBacking roll-up in R333’s Enum facts) from producers instead. The remaining failure mode is genuine, not a config artifact: two use sites of one enum resolving to different backings (one-enum-one-type), which surfaces as a typed rejection. Note the backing need not be a Java enum: an SDL enum may map to a String (varchar) or numeric (integer) column, so the inference generalizes over backingType, not just jOOQ-generated enum classes. (updated 2026-06-23)

  • R133 Flip leaf-coverage profile activation to opt-in: The leaf-coverage profile in graphitron-rewrite/pom.xml is activated by negation (<name>!leaf-coverage.skip</name>), so every default contributor mvn verify truncates target/leaf-coverage.jsonl in process-test-resources and threads a graphitron.classification.trace system property into every surefire/failsafe. The traces are only consumed by roadmap-tool leaf-coverage, which after R132 runs only in the CI regeneration step. Every other build pays the antrun-truncate cost and writes JSONL nobody reads.

  • R359 Guard ColumnRef.sqlName() comparisons against case-sensitivity drift: The column-identity sibling of R358, filed by R358 so the defect does not fall into a blind spot silently. ColumnRef.sqlName() is, like TableRef.tableName(), a case-preserved verbatim identity string, while the jOOQ catalog’s findColumn lookup (JooqCatalog.java:813) is the already-case-insensitive layer (the column analogue of findTable). So the same structural drift exists: one live case-sensitive .sqlName().equals( (GraphitronSchemaValidator.java:883, !rcf.column().sqlName().equals(ocf.column().sqlName())) sits alongside six equalsIgnoreCase comparison sites (FieldBuilder.java, TypeBuilder.java, BuildContext.java, NodeIdLeafResolver.java ×3). R358 scoped itself to tableName and explicitly excluded this; the proportionate fix mirrors R358’s Phase 2: a sameColumn(…​)-style predicate on ColumnRef plus a guard scan, or fold both identity strings into one canonical-identity pass (R358 "Alternatives considered"). Per-site reachability: :883’s operands may, like R358’s `:3105, be non-divergent (it compares two `ColumnRef`s that may share provenance); this item makes that per-site call rather than presuming the defect is live. (updated 2026-06-23)

  • R54 Rename @externalField (parallel-support, deprecation, migration): @externalField lifted to IMPLEMENTED_LEAVES end-to-end in computed-field-with-reference (R48, shipped; see [changelog.md](changelog.md)). The directive’s name is the surviving historical artefact: it predates the ChildField.ComputedField model variant and reads as "field resolved by external code" rather than the narrower behaviour the lift settled on (a Field<X> returned by a static method, inlined into the SELECT projection at the alias). A clearer name ships in this plan; the old name stays accepted for one consumer-migration window.

  • R27 Retire @nodeId and IdReferenceField synthesis shims: Two parallel shims survive in the classifier so legacy SDL keeps building. Both should retire on the same gate (sis migration to canonical SDL); their wire shape is independent but the user-visible migration is one piece of work, so the two retirements ship together. (blocked by sis-rewrite-migration)

  • R51 Split PropertyField/RecordField on parent-kind instead of nullable column: ChildField.PropertyField and ChildField.RecordField each carry both columnName: String and column: ColumnRef, with column nullable depending on the parent type: non-null when the parent is a JooqTableRecordType with a resolvable column, null for JooqRecordType / JavaRecordType / PojoResultType parents. The single record straddles two parent kinds via an Optional component, leaving columnName as the only carrier of the SDL string when column is absent. Per Narrow component types over broad interfaces and Sub-taxonomies for resolution outcomes, the right shape is two sealed-arm variants (one for table-backed parents carrying a non-null ColumnRef, one for non-table-backed parents carrying just the SDL string), not one record with a nullable component. Split surfaced during R50’s columnName cleanup on ChildField.ColumnField / ColumnReferenceField, where the table-backed-only invariant let those carriers retire columnName outright; this item carries the same rigour to PropertyField and RecordField.

  • R235 Tidy @reference path-element surface: separate join-shape from WHERE-filter: The legacy ReferenceElement { table, key, condition } directive surface combines three roles in one input object: key: and table: and condition: (without companions) name the join shape; condition: combined with key: or table: names a WHERE-filter that folds onto the FkJoin’s whereFilter. The full combinations table at R232’s spec lines 44-52 documents seven valid shapes. The conflation invites cargo-culting (condition: sometimes means "ConditionJoin", sometimes "WHERE filter on FkJoin") and the seven-shape free combination invites authoring drift. (updated 2026-05-23)

  • R17 Annotated walkthrough of a generated file: Today’s docs cover the input side (schema → classification → variant) and the model side (sealed hierarchy, capability interfaces, design principles) but a contributor reading them never sees a complete generated file explained section by section. The mental model "this is what the output looks like" gets reconstructed from grepping graphitron-test/target/.

  • R76 Emit per-participant fieldsJoin and orderBy; replace SelectJoinStep mutation in interface fetchers: TypeFetcherGenerator.buildQueryTableInterfaceFieldFetcher and buildTableInterfaceFieldFetcher emit dynamic jOOQ queries by declaring a SelectJoinStep<Record> step local and reassigning it inside if (alias != null) blocks (buildCrossTableJoinChain, see [TypeFetcherGenerator.java:686-700](../graphitron/src/main/java/no/sikt/graphitron/rewrite/generators/TypeFetcherGenerator.java) and [TypeFetcherGenerator.java:759-770](../graphitron/src/main/java/no/sikt/graphitron/rewrite/generators/TypeFetcherGenerator.java)). That is not the idiomatic jOOQ pattern for dynamic joins; jOOQ’s documented form folds the conditional join into a single fluent expression via DSL.noTable() / DSL.noCondition(), which jOOQ erases at render time. The step-mutation form also centralises join construction in QueryFetchers, breaking symmetry with the existing per-type-class $fields(…​) helper (which already gates SELECT entries by selection set on the participant type).

  • R417 Reconcile sakila-example README app-section with R399 (dead GraphqlEngine/GraphqlResource/AppContext links): graphitron-sakila-example/README.md still describes the runtime under a "Runnable reference (the app)" section (roughly lines 17-27) as three example-owned files: GraphqlEngine.java, GraphqlResource.java, and AppContext.java. R399 extracted all of that into graphitron-jakarta-rest; the example now ships a single SakilaGraphitronApplication adapter (a GraphitronApplication SPI implementation) and depends on the library for the /graphql resource, the engine, status-code semantics, the /schema endpoint, and the GraphiQL page. The three source links in that section are dead, and the "three files cover the runtime" framing is false. (updated 2026-07-01)

  • R47 Short class-name resolution for @service and @externalField (legacy parity): ServiceCatalog.reflectServiceMethod currently calls Class.forName(className) directly, forcing an FQN. Existing schemas carry short class names like className: "PersonService" and rely on the legacy Mojo’s externalReferenceImports list to find them. Without short-name resolution, every legacy schema has to be migrated to FQNs at the same time as it migrates to the rewrite, which is unnecessary friction.

  • R35 Class-level Javadoc and package-info.java sweep: A reader landing on FieldBuilder.java (2 172 lines) or TypeFetcherGenerator.java (1 646 lines) gets minimal in-file orientation; they have to bounce to the docs to learn what the class is for. The rewrite tree also has zero package-info.java files, which is the IDE-native place for "what is in this package" blurbs.

  • R116 Cover composite-key Row2 path-keyed @sourceRow classification: R110 shipped @sourceRow with Row2..Row22 arity admitted by the resolver: the per-position type loop in SourceRowDirectiveResolver iterates the lifter’s RowN type arguments without special-casing arity 1, and the leaf-PK arm constructs LifterLeafKeyed over whatever the leaf’s TableRef.primaryKeyColumns() returns. The existing SourceRowClassificationCase test enum exercises Row2 only on the rejection path (LEAF_PK_ARITY_MISMATCH against inventory.inventory_id); no successful Row2 path-keyed classification fires anywhere in the test corpus today. The gap is in the test catalog (no 2-column FK exists in graphitron-rewrite/graphitron/src/test/…​), not in resolver / emitter code.

  • R120 Drop or wire FkJoin.alias dead storage: BuildContext.synthesizeFkJoin (BuildContext.java:694) populates FkJoin.alias as fieldName + "_" + stepIndex (e.g. "language_0") while resolving a @reference path. No code reads it: JoinPathEmitter.generateAliases (JoinPathEmitter.java:41) derives its own per-hop aliases from the target table’s javaClassName() + hop index, and emitters layer their own runtime prefixes for self-ref recursion uniqueness on top. The stored value is never consulted. Same applies to the sibling ConditionJoin.alias slot.

  • R126 Scrub residual BatchKey.X references from sakila-service / sakila-example prose: R38’s Phase 3 follow-up ("Stale-prose scrub", commit 5d82380) claimed 118 BatchKey references in Javadoc / comment prose were cleaned up, but several sites in the sakila-service test fixtures, sakila-example execution test, and the example schema.graphqls description comments still mention deleted BatchKey.X permits (some as dead {@link no.sikt.graphitron.rewrite.model.BatchKey.X} Javadoc, some as plain prose). Build is green (Javadoc lookups are best-effort), but the references are noise for readers and trip code search.

  • R10 Drop the assembled-schema rebuild in favour of per-variant graphql-java forms: Phase 5 of [firstclass-connection-types](firstclass-connection-types.md) rebuilds the assembled GraphQLSchema via SchemaTransformer so directive-driven @asConnection carriers carry their rewritten return type and pagination args. The rebuild only runs at generate time and is never seen by the runtime (which reconstructs its schema from emitted <TypeName>Type.type() calls in GraphitronSchema.build()).

  • R24 NodeIdReferenceField JOIN-projection form: R50 shipped two of the three rooted shapes named in Variant-by-variant collapse → Single-hop emission, two shapes: rooted-at-child emission (FK-mirror, no JOIN, parent’s FK columns encode directly) and the classifier-side resolution for rooted-at-parent (phase g-B produces ChildField.ColumnReferenceField / CompositeColumnReferenceField with compaction = NodeIdEncodeKeys and a resolved joinPath). What did not ship is the matching emitter: FetcherEmitter#dataFetcherValue carries runtime UnsupportedOperationException stubs for both arms (lines 140-162), so a schema that reaches one of the rooted-at-parent shapes builds without a validator-side rejection but throws at runtime.

  • R34 sis-graphql-spec migration to graphitron-rewrite: Track the consumer-side schema work needed to bring sis-graphql-spec cleanly onto graphitron-rewrite. This plan exists because sis is the canonical large-scale consumer; closing it out validates the rewrite’s classification contracts end-to-end and lets us close courtesy windows on shims (notably [retire-synthesis-shims](retire-synthesis-shims.md), which gates on this work).

  • R280 Typed non-empty carrier for fetcher-registration bodies: Backlog stub. Spun out of R166 (graphqlschemavisitor-driven-emission, retired into R279) to keep a parked micro-refactor alive; originally surfaced as R165 (fetcher-registration-empty-body-filter) and flagged by the principles-architect read on R165 as the natural endpoint its Optional<CodeBlock> deferral should not lose track of. (updated 2026-06-05)

  • R168 Sub-agent classifier for blast-radius effort (Low/Medium/High) at Spec stage: The roadmap roll-up sorts by priority: but carries no signal about how big a piece of work each Active item is. A reader scanning the table cannot tell whether Ready means "one afternoon" or "a multi-phase lift across four modules", and the author setting priority: is making that judgement implicitly without surfacing it. Add an effort: front-matter field with values Low | Medium | High defined as blast radius (files touched, design forks, test tiers reached), populated by a sub-agent classifier at the Spec stage (Backlog stubs are too thin to grade against and explicitly do not carry the field). The classifier reads one plan file at a time and emits a single bucket, so a batch reclassify across ~30 Active items costs roughly one normal turn’s worth of tokens. Render the field as a column on the Active table in roadmap/README.md and as an attribute in the per-plan AsciiDoc page; gate the validator so an effort: value on a Backlog item is a hard error. (updated 2026-05-16)

  • R85 Emit graphitronContext helper into Conditions and Type classes: @condition(contextArguments: […​]) is a documented feature (docs/getting-started.adoc:198,226, runtime-extension-points.adoc:96-101) but its generated output does not compile. The classifier produces CallSiteExtraction.ContextArg for context-bound filter parameters (MethodRef.java:111, exercised at GraphitronSchemaBuilderTest.java:2768); ArgCallEmitter.buildArgExtraction’s `ContextArg arm emits graphitronContext(env).getContextArgument(env, …​); the call lands in <RootType>Conditions.<field>Condition() (via QueryConditionsGenerator) or in <TypeName>.$fields() (via InlineTableFieldEmitter / InlineLookupTableFieldEmitter). Neither host class emits a graphitronContext helper, so the generated source fails at mvn compile -pl :graphitron-sakila-example with "cannot find symbol: graphitronContext". The bug is currently latent because no fixture in graphitron-sakila-example or graphitron-fixtures-codegen uses contextArguments on @condition.

  • R208 Retire the @asConnection(connectionName:) deprecated argument: @asConnection(connectionName:) is deprecated in directives.graphqls (the SDL @deprecated marker landed alongside R93’s SdlAction migration registry; see directives.graphqls:243) but still functional: ConnectionPromoter.resolveConnectionName honours an explicit override when present and falls back to the <ParentType><FieldName>Connection derivation otherwise. The deprecation reason states the architectural concern: sharing one synthesised type across distinct carrier fields conflates parents / filters / orders at the type level, and the override exists "only as a transition mechanism for legacy schemas". (updated 2026-05-21)

  • R245 Wire @condition through to mutation WHERE (emit half + new placements): @condition on mutations is half-built today: MutationInputResolver.java admits input-field-level @condition(override: true) (R215, lines ~482-498) but the directive is a no-op at emit (no .where(…​) clause is produced). Argument-level @condition on a non-@table mutation argument is rejected outright (line 446). Input-field-level @condition without override: is rejected. This item closes the emit half and lifts the two admission rejections so the directive does something useful. (updated 2026-05-27)

Validation

  • R136 Execution-tier coverage for FK-target/NodeType-keyColumns permutation: R131’s permutation relaxation is pinned at the pipeline tier (InputFieldFkTargetNodeIdCase.FK_TARGET_REORDERED_KEY_PERMUTATION_DIRECT_FK{,_SINGULAR} in NodeIdPipelineTest), which asserts liftedSourceColumns is permuted into @node.keyColumns order on the resolver’s DirectFk carrier. The end-to-end SQL correctness — that the emitted BodyParam.RowEq against liftedSourceColumns actually matches the right rows when joined against decoded NodeId values — is not exercised by an execution-tier test in this repo.

  • R135 Multi-hop @nodeId pipeline test for FK-target/NodeType-keyColumns permutation: R131’s permutation relaxation in NodeIdLeafResolver.resolve accepts set-equality between the terminal hop’s target columns and the NodeType’s @node(keyColumns:), then permutes liftedSourceColumns into NodeType-keyColumns order before constructing Resolved.FkTarget.DirectFk. The pipeline-tier test pinning this lands on the single-hop reordered_pk_parent fixture (InputFieldFkTargetNodeIdCase.FK_TARGET_REORDERED_KEY_PERMUTATION_DIRECT_FK{,_SINGULAR}).

  • R181 Validate @order/@defaultOrder: empty directive and @index coexistence: A real user report (paraphrased) crashed the schema build: (updated 2026-05-20, created 2026-05-19)

  • R107 Classify leaf mentions in inference-axis-coverage report: LeafCoverageReport.parseMentions (R104) joins each sealed leaf simple-name against every roadmap *.md body via a \b<simpleName>\b regex. The match is undifferentiated: backticked code spans, code-fenced blocks, and bare prose mentions all collapse into the same Roadmap cell. Two consequences. First, every roadmap edit that names a leaf in any form drifts inference-axis-coverage.adoc and trips the verify-leaf-coverage-report CI gate, which is the regen-friction tax R104 deferred. Second, a reviewer reading the column has no way to sanity-check a match — Field against FieldType is excluded by \b, but a phrase like "the field type" cannot be told apart from a deliberate Field symbol reference without re-reading the source spec body.

  • R419 Reject list-valued @nodeId+@reference carriers on INSERT inputs at build time: A list-valued node-id reference field on an INSERT input (e.g. parentId: [ID!]! @nodeId(typeName: "T") @reference(path: […​])) passes classification and validation today: NodeIdLeafResolver is arity-agnostic, BuildContext.classifyInputField just bakes list=true into the ColumnReferenceField / CompositeColumnReferenceField carrier, and MutationInputResolver.admitMutationInputFields admits reference carriers for INSERT unconditionally (only the NestingField arm rejects lists). The generated code compiles but hardcodes single-value assumptions (instanceof String decode guard, .value1() bind in TypeFetcherGenerator), so any non-empty list value throws GraphitronClientException "Decoded NodeId did not match the expected type" at runtime. That is the worst failure mode: it surfaces only when the field is populated. Until fan-out semantics are actually supported (R420), a list() guard in admitMutationInputFields alongside the existing nesting-field list rejection should turn this into a clear build-time schema error. (updated 2026-07-02)

Other

  • R236 BuildContext nested-input candidate-hint draws from path-origin table instead of @reference terminal table: BuildContext.classifyInputFieldInternal (BuildContext.java:1665-1677) emits a "Did you mean…" hint when a nested-input column name is unresolvable. The candidate list is built from catalog.columnSqlNamesOf(resolvedTable.tableName()) where resolvedTable is the path-origin enclosing input’s @table, not the path’s terminal table. (updated 2026-05-23)

  • R373 Capture test stdout/stderr to per-class files via Surefire redirectTestOutputToFile: Several tests emit info/warn/error logs (SLF4J + Logback ConsoleAppenderSystem.out) and stack traces during a normal mvn install run, drowning the console in noise that is irrelevant when the test passes. There is no per-test "show only on failure" buffering in plain Surefire, but redirectTestOutputToFile=true captures each test class’s stdout/stderr into target/surefire-reports/<TestClass>-output.txt, keeping the reactor console clean while preserving the full output on disk for any class whose tests fail. Apply it once in the parent pom (graphitron-rewrite/pom.xml) so every module inherits it. (updated 2026-06-25)

  • R432 Collapse SplitTableField and RecordTableField into one source-gated leaf: The R333 beachhead: collapse ChildField.SplitTableField and ChildField.RecordTableField into one leaf gated on the source fact. Thread A measured the two component-identical (11 shared components, both TableTargetField + BatchKeyField; only emitsSingleRecordPerKey() and sourceShape() differ), and both child sides already lower to the same load<X> rows-method and fetcher; Split’s only extra, the parent-key projection, already lands via collectRequiredProjectionColumns in the parent type’s $fields. Collapsing with zero residue retires one cross-product axis with no generator rewrite and produces the lowering’s first executable proof (R333 "First slice"). (updated 2026-07-04) (blocked by decompose-sourcekey)

  • R297 Collapse the shareable boolean on ConnectionType/EdgeType/PageInfoType; read federation flags off schemaType(): ConnectionType / EdgeType / PageInfoType each carry a shareable boolean component alongside their schemaType() GraphQLObjectType. After R295, federation @tag propagation onto these synthesised types is driven entirely off schemaType() (the applied directives ride on the schema form; no tags record component, per "Model metadata over parallel type systems"). The shareable boolean is now the asymmetric survivor: it is a second representation of @shareable, redundant with the schema form for emission (the synthesised schemaType() already carries the directive, and no emitter reads shareable()), and its only consumer is the pageInfoShareable |= fold inside ConnectionPromoter.promote. That fold also reads tags off the schema forms, so it mixes two representations of the same class of information in one place. (updated 2026-06-10)

  • R289 Correct KeyNodeSynthesiser opt-out javadoc: @key(resolvable: false) does not keep a type out of Entity: KeyNodeSynthesiser’s class javadoc carries an "Opt-out" paragraph claiming that a consumer who writes `@key(fields: "id", resolvable: false) on a @node type "keeps it out of _Entity`". That is false for the pinned federation-jvm version: `Federation.transform injects every @key-bearing type into the _Entity union regardless of resolvable:. The behaviour is empirically pinned by FederationBuildSmokeTest.resultEntityUnionContainsAllFixtureEntities, which asserts both the table-bound Language stub and (since R286) the non-table-bound FilmRefStub, both @key(resolvable: false), are present in the served _Entity union. resolvable: false does not suppress union membership; what it does is tell the supergraph composer not to route entity-resolution queries to this subgraph for that type. This is the "broader failure mode" the Documentation names only live tests/code principle warns about: a doc claim that a live test directly contradicts, surfaced during the R286 (12a9f88 + 77362d3) In Review → Done review. Surface flagged, not introduced by R286 (the test’s prior javadoc already noted "federation still includes them in the union"). Fix: rewrite the opt-out paragraph to describe what resolvable: false actually does (composer routing, not union membership), and audit KeyNodeSynthesiser:22 ("surfaces them in `_Entity`") for the same precision. No code change; doc-only, but verify no other javadoc/spec prose repeats the false claim. _(updated 2026-06-09)

  • R431 Decompose SourceKey onto the model’s facts: SourceKey is (target, columns, path, wrap, cardinality, reader) and bundles three separable concerns, only one of which is a source key (R222 "What SourceKey decomposes into"; R333 sharpens the destinations). This item is the eager, mechanical decomposition, sequenced ahead of the reentry emit re-platforming (R314) so the emit slices land on decomposed facts instead of extending the conflated record. (updated 2026-07-08, created 2026-07-04)

  • R393 Disambiguate the base-to-detail (interface-to-implementer) join path via @reference: R389 ships first-class discriminated joined-table inheritance: a participant declares its own detail @table and its base-only inherited fields carry @reference back to the discriminated base. Two paths ride that declared reference: resolving the inherited (base-resident) fields (detail to base), and the base-to-detail join the interface fetcher emits to reach each implementer’s own detail table (base to detail, the interface-to-implementer path). R389 handles the unambiguous shape only: exactly one foreign key connects the detail table and the base, so the reference pins the join with nothing to disambiguate. Both R389 fixtures are this shape (party_individual → party; jti_app_account → jti_subject). (updated 2026-06-26)

  • R209 FieldRegistry classify-input trace loses typed Rejection payload: FieldRegistry.classifyInput at graphitron/src/main/java/no/sikt/graphitron/rewrite/FieldRegistry.java:108-110 emits the trace record for an InputFieldResolution.Unresolved outcome by defaulting to RejectionKind.AUTHOR_ERROR with u.reason() (a String), with the rationale comment "Unresolved carries no Rejection variant …​ default to AUTHOR_ERROR per the kind-of-thumb rule". This is the last place in the input-classification path where the typed-rejection chain breaks: the trace consumers (watch-mode formatter, LSP fix-its) lose the structured attempt + candidates payload they would otherwise consume on a column-miss Unresolved, and on non-column-miss Unresolved they get an AUTHOR_ERROR label that may not match the actual rejection kind. R205 closed the gap one layer up (InputFieldResolver.resolve now lifts to typed Rejection.unknownColumn / Rejection.structural); the corresponding lift inside FieldRegistry.classifyInput was flagged in the R205 self-review and deferred. Two design forks worth thinking through during Spec: (a) widen InputFieldResolution.Unresolved to carry a Rejection (touches every Unresolved construction site in the classifier; some sites lack catalog/candidates context to build unknownColumn), or (b) thread TableRef rt into FieldRegistry.classifyInput and lift to Rejection there. (a) keeps the lift co-located with classification; (b) keeps Unresolved transient by design. Either way the deliverable is removing the RejectionKind.AUTHOR_ERROR default arm and emitting RejectionKind.of(rejection) consistently with traceOutput at FieldRegistry.java:127-130. (updated 2026-05-21)

  • R298 First-client contract-composition check: do type-level @tag-only synthesised connection types satisfy a real federation contract?: R295 propagates the carrier field’s federation @tag applications onto the synthesised Connection, Edge, and PageInfo type declarations (the minimal shape that satisfies contract composition as the bug was framed). Its "Resolved" section deferred a verification step that could not run at spec time and could not run in the implementation/review sandbox either: building a real Apollo Federation contract that includes the carrier’s tag and confirming composition succeeds against type-level-only tags. The risk this check covers: TagApplier tags fields, input fields, enum values, args, and unions but never type declarations (see its class javadoc), so under <schemaInput tag> the synthesised types carry the only type-level tags in the graph, while legacy contracts were validated against field-level tags. The green federation-SDL round-trip test in ConnectionFederationTagPipelineTest proves the tags are emitted on the types, but a round-trip is not a contract build: a real contract can still reject type-level-only tags. The outstanding work: run the first-client contract build; if (and only if) type-level tags prove insufficient, tag the synthesised types' fields (edges, nodes, pageInfo, totalCount, cursor, node) as well. Landed under R295 (fae7c6f, 1fdcf18); this item carries the residual federation-contract risk R295 explicitly could not close in-session. (updated 2026-06-10)

  • R334 Generated @condition arg extraction is an unreadable nested-ternary one-liner: Every generated @condition argument is inlined as a nested ternary directly in the WHERE chain, e.g. env.getArgument("filter") instanceof Map<?, ?> map1 ? (String) map1.get("brukerId") : null, repeated once per argument across a single .and(…​) term. A method with several condition args renders as one dense, hard-to-read, hard-to-breakpoint expression (flagged by a consumer as "an eyesore" that "violates our principle of readable and debuggable code"). R330 made the surrounding .and(…​) chains and the FK-target EXISTS multi-line, but did not touch the per-argument extraction, which is cross-cutting: it lives in ArgCallEmitter.buildArgExtraction and feeds every WHERE-emitting site (the QueryConditions shim plus the inline / lookup / split fetcher emitters). The fix likely extracts each argument into a named local (a clearly-typed var <name> = …​) before the call, or routes through a small generated helper, so the call site reads as Conditions.method(table, brukerId) and each extraction is independently debuggable. Scope: emitter-only, generated output changes shape but not behaviour; pipeline tests must not assert on generated method bodies, so coverage stays at the compile/execution tier. (updated 2026-06-18)

  • R118 Graphitron MCP server programme: agent-facing schema, catalog, code, and docs tools in graphitron:dev: This item is a programme, not a single deliverable. It absorbs the earlier single-feature R118 (live catalog discovery + docs RAG) and widens it into the full agent-facing surface of the MCP server embedded in graphitron:dev. The foundational use case is unchanged and still anchors the programme: greenfield onboarding, where a developer points graphitron at a large existing database with an empty (or nearly empty) GraphQL schema and needs to discover what exists before authoring anything. The programme generalises that into a standing capability: any MCP-aware client (Claude Code, Cursor, others) can discover the database catalog, the hand-written Java the schema wires to, the current schema and its classifications, the directive grammar, and the live diagnostics, plus semantic search over the catalog and the bundled documentation, all without a hosted service, an external database, or an API key.

  • R201 Honor @field(name:) in @error payload construction shape resolution: FieldBuilder.resolvePayloadConstructionShape (FieldBuilder.java:506-609) picks an @error payload class’s construction shape, then the emitter (catch-arm payloadFactoryLambda in TypeFetcherGenerator, and the validator pre-step’s declareEarlyPayloadFromErrors) generates either an all-fields-ctor invocation or a no-arg-ctor + per-SDL-field setter sequence against it. Neither arm reads @field. The mutable-bean arm at :589-591 matches set<UcFirst(sdlFieldName)> on payloadCls.getMethods() via the Java-bean conversion in javaBeanSetterName; the classifier’s existing invariant at :498-505 pins "setter method name matches the SDL field name under Java-bean conversion" as a contract the emitter relies on. The all-fields-ctor arm at :519-535 picks the single ctor whose parameter count equals sdlFieldNames.size() and the emitter then assumes positional alignment with SDL declaration order. Either way, a payload class whose Java component or setter names diverge from the SDL field names has no remap — exactly the shape R191 now admits on data fields. The fix is to thread @field(name:) on each SDL field into both arms: in the mutable-bean arm, derive the setter base from the directive value when present (set<UcFirst(directiveName)>) and persist the SDL-to-Java mapping on SetterBinding; in the all-fields-ctor arm, the directive value picks the matching parameter (by name where the ctor exposes parameter names, otherwise by the directive’s positional alignment with the record component / canonical-ctor parameter order). Update the classifier invariant text at :498-505 to reflect the new contract (setter / parameter name matches the directive value when present, the SDL field name otherwise). This is the output-side mirror of R200; together they restore symmetry across input bean / record binding (R200) and output payload bean / record construction (this item). (updated 2026-05-20)

  • R202 Honor @field(name:) in @error type extra-field accessor matching against handler source class: FieldBuilder.checkErrorTypeSourceAccessors (FieldBuilder.java:2281-2316) verifies that each @error object type’s extra fields (everything except path / message) can be populated from the handler’s source class (the exception class for a GENERIC handler, the DataAccessException-shaped source for a DATABASE handler, etc.). For each (sdlField, sourceClass) pair it calls ClassAccessorResolver.resolve(sourceClass, sdlField.getName(), expectedReturn, …) with the raw SDL name as the accessor base. When the exception’s accessor diverges from the SDL field name — e.g. an exception exposing getErrorCode() mapped to an SDL field named code, or a Norwegian-named accessor under an English SDL — the resolver returns Rejected and the type fails classification with no author escape hatch. @field(name:) on the SDL extra field is the natural override: this is structurally the R191 case (free-form Java class as the logical "parent", SDL field bound by accessor name, divergent names need a directive remap). The fix is to read DIR_FIELD on each extra sdlField at :2298-2305 and pass the directive value (when present) as the second arg to ClassAccessorResolver.resolve instead of sdlField.getName(). The directive’s docstring already covers this site under the "underlying-binding target" reading; the docstring update tracked separately should mention @error extra fields as one of the free-form-Java-class accessor-axis examples. No emitter change needed — ClassAccessorResolver.Resolved.methodName() is the actual reflected method name, so the runtime invocation path is unaffected. (updated 2026-05-20)

  • R69 Implement @experimental_constructType: The @experimental_constructType(selection: "…​") directive is declared in directives.graphqls and stripped from the emitted schema by SchemaDirectiveRegistry, but no classifier, model carrier, or emitter exists for it yet.

  • R288 Inline TableInterfaceField and TableMethodField children (currently N+1): A child @table field backed by a polymorphic interface target (ChildField.TableInterfaceField) or by a @tableMethod (ChildField.TableMethodField) is reachable by FK correlation from a query-scope parent, so it should inline into the parent query as a correlated subquery / DSL.multiset(…​), exactly like the ordinary ChildField.TableField (InlineTableFieldEmitter). It does not. Both leaves get a generated synchronous fetcher method (TypeFetcherGenerator.buildTableInterfaceFieldFetcher, buildChildTableMethodFetcher) that runs its own per-parent dsl.select(…​).from(…​).where(parent-correlation).fetch() against env.getSource(). There is no SplitRowsMethodEmitter and no DataLoader registration for these, so the fetcher fires once per parent row: an N+1 query pattern. N+1 is never correct. (updated 2026-06-09)

  • R430 LSP publishes graphitron:dev compile diagnostics against generated-file URIs: R410’s Surfacing compile diagnostics section named three consumers for the incremental-compile round’s diagnostics: the console dev-loop block, the MCP diagnostics tool, and the LSP publishing them against the generated file’s URI (best-effort, so an editor with that generated .java open shows the javac error inline). R410 shipped the first two; the diagnostics already land on Workspace.compileDiagnostics() (in the LSP module) after every round, but no LSP textDocument/publishDiagnostics is emitted for them. Close the gap: on each setCompileDiagnostics swap, publish the round’s error diagnostics against the generated-file URIs (resolving CompileDiagnostic.file() under the generated-sources root) and clear diagnostics that resolved. Best-effort per the R410 spec: an unresolvable path is skipped, not an error. (updated 2026-07-03)

  • R382 Lower orderBy onto multitable-interface/union queries: A root query field returning a multitable interface or union (QueryField.QueryInterfaceField / QueryField.QueryUnionField, and the @asConnection variant) cannot carry a user-specified ordering. operation() hardcodes new OrderBySpec.None() for both arms, and the emitter orders results solely by the synthetic sort key (the participant PK). A consumer asking for a specific order gets PK order regardless. (updated 2026-06-25)

  • R387 Migrate TypeConditionsGeneratorTest off code-string assertions on generated method bodies: TypeConditionsGeneratorTest asserts on generated condition-method bodies via method.code().toString() + AssertJ contains(…​) on literal jOOQ expression strings (e.g. assertThat(body).contains("table.FILM_ID.in(ids)")). This contradicts the rewrite design principle that code-string assertions on generated method bodies are banned at every tier (development-principles.adoc § "Behaviour is pinned at the pipeline tier and above"): such assertions test implementation rather than behaviour and break on every emitter refactor. The pattern predates R380 (the existing cases trace to R375 / R79 / R50) and R380 extended it for the RemoteColumnPredicate / correlated-EXISTS arm; in every case the emitted behavior is already proven at the execution tier (GraphQLQueryTest, real rows against seeded PostgreSQL) and locked at the compilation tier (graphitron-sakila-example against real jOOQ classes), so the code-string cases are redundant maintenance burden. Migrate the whole file: replace body-string assertions with structural assertions (method/parameter shape, TypeSpec / MethodSpec structure — the paramType / call-type delegation tests already model this) and lean on the execution + compilation tiers for body correctness. Scope is test-only; no generator or model change. Worth confirming during Spec whether any body-shape invariant (e.g. the EXISTS correlation direction or terminal-alias binding) is genuinely not observable at the execution tier and therefore needs a non-string structural pin rather than outright removal. (updated 2026-06-26)

  • R462 Model the nested fetcher own outgoing per-field precise edges in CompileDependencyGraphBuilder: R459 registered the <Type>Fetchers node for a fetcher-owning plain-object nesting type and got the schema-shape-to-fetcher wiring edge (FilmMetaType → FilmMetaFetchers) for free. It deliberately did not model the nested fetcher’s own outgoing per-field edges: a nested SplitTableField’s DataLoader methods reference the target type’s projection class; a nested composite / `@nodeId read references NodeIdEncoder. CompileDependencyGraphBuilder.addFieldEdges never sees these fields because they are absent from schema.fields() (and schema.fieldsOf(nestedType) is empty for a coordinate-less nesting type). The TypeSpecReferenceWalk completeness oracle will flag any such edge as a superset gap once a harness fixture exercises it. (updated 2026-07-10)

  • R252 Multi-file federation fixture coverage for schema.graphqls emission: R247’s SchemaSdlEmitter writes target/generated-resources/…​/schema.graphqls by calling ServiceSDLPrinter.generateServiceSDLV2(assembled) on the codegen-time schema. The only pipeline-tier coverage is FederationBuildSmokeTest against sakila’s single-file federated fixture (federated-schema.graphqls, which carries extend schema @link(…​) and types in the same file). Real-world consumer schemas often split across many files — one carrying extend schema @link(…​), others carrying types only, no explicit schema { …​ } block anywhere — and that shape goes through a different code path in RewriteSchemaLoader (MultiSourceReader concatenates streams, single Parser.parseDocument, single SchemaParser.buildRegistry). (updated 2026-05-27)

  • R249 Nested @argMapping syntax via GraphQLSelectionParser: Today’s @argMapping parses as comma-separated javaParam: <path> entries where each right-hand side is a dot-path into the SDL args. To construct a Java record or JavaBean whose fields come from scattered SDL positions (rather than a single anchored input-object), authors have no syntactic recourse: they must either restructure the SDL to mirror their Java type, or carry the binding logic into the service method itself. (updated 2026-05-27)

  • R412 Nested backing class emits $-qualified names at the no-Class-in-hand emit sites (backingClassOf, recordColumnReadArgs, FetcherEmitter, ChildField): R370 fixed the ClassName.bestGuess(binaryName) nested-class defect (a nested backing class emits the non-compiling Outer$Nested instead of the JLS-legal Outer.Nested) at the four sites that had a structurally-correct name already in hand: the two AccessorRef producers, the @service return-type validator (checkServiceReturnMatchesPayload), and the @service fetcher return type (computeServiceRecordReturnType). The same bestGuess-over-fqClassName() hazard recurs at a family of emit sites that hold no reflected Class<?> and no captured structural TypeName, so they cannot take R370’s one-for-one call swap: (updated 2026-07-01)

  • R274 OutcomeType carries its success projection so the nullability invariant lives on the carrier: R244’s spec frames OutcomeType as a carrier whose successProjection holds "the non-errors data fields, all nullable (enforced)" so that "possessing an OutcomeType is the proof those invariants hold" (the mirror-the-classifier / generation-thinking stance: the type carries what consumers rely on). The R244 implementation does not honour that: at FieldBuilder.resolveServiceOutcomeChannel (FieldBuilder.java:~2106) the OutcomeType is constructed with successProjection = List.of(), and the nullable-success-projection invariant (NonNullableSuccessProjectionField) is instead enforced by an inline loop over payloadObj.getFieldDefinitions() before construction. So the invariant lives in a loop, not the type, and the OutcomeType value is not the proof it is documented to be. (Surfaced by the principles-architect review of the R244 rework, finding E1; the rework annotated the empty list rather than re-architecting, deferring the consolidation here.) (updated 2026-06-02)

  • R213 Plain-input field rejections attributed to consumer field, losing input-field source location: When a plain (non-@table) input type carries a broken @condition directive (parameter-binding mismatch, reflection failure, etc.) or an unresolvable column, the failure surfaces as an UnclassifiedField on the consuming field (e.g. Query.opptak), not on the input type’s offending field (e.g. OpptakFilterInput.opptaksNavn). The reported SourceLocation is the consuming field’s definition, so LSP fix-its, watch-mode formatters, and editor highlights point one indirection away from the actual broken directive the author needs to edit. (updated 2026-05-21)

  • R278 Polymorphic type classification: sealed union-type variants over ParticipantRef: Resurrected. This item originally proposed classifying polymorphic types (interface/union) "in field context" by enriching the ParticipantRef participant-list model. It was briefly absorbed into R279 (field-first-classification-driver) as a set of ParticipantRef tweaks, then pulled back out: the participant-list-with-role-flags shape is itself suspect, and R279 is a behaviour-preserving driver restructure that should not also redesign polymorphic classification. This item is re-scoped to that redesign and held at Backlog to iterate the design. (updated 2026-06-05, created 2026-06-04)

  • R443 Post-R438 stale-reference residue: ConditionResolution javadoc + fkjoin-alias-dead-storage item: R438’s In Review to Done review surfaced two staleness residues its own stale-reference sweep did not cover; captured here as a small cleanup since R438 is closed. (updated 2026-07-08)

  • R348 Regenerate and guard the generated supported-schema-shapes migration doc against drift: docs/manual/generated/supported-schema-shapes.adoc is a generated migration fragment (included by docs/manual/how-to/migrating-from-legacy.adoc) produced by graphitron-roadmap-tool leaf-coverage --mode=migration (LeafCoverageReport.run, migration branch). Like its sibling supported-directives.adoc (R346), its header promises "Regenerate via the verify-mode CI guard," but no such guard exists: nothing in .github/workflows, the poms, or any script runs leaf-coverage --mode=migration. The only leaf-coverage step in CI (rewrite-build.yml "Regenerate leaf-coverage report") runs leaf-coverage graphitron-rewrite with no --mode=migration, regenerating the internal inference-axis-coverage.adoc report, not this fragment. So the fragment silently drifts whenever the classified leaf set changes, and a reader migrating off legacy is shown a stale schema-shape support surface. _(updated 2026-06-19)

  • R302 Rename ChildField to SourceField (carrier-named field hierarchy): Split out of R290 (datafetcher-field-dimensional-slots). R222’s refined field-side model names the three carriers after their GraphQL parent-type category: QueryQueryField, MutationMutationField, and SourceSourceField. The first two already match their carrier; the third is still called ChildField, a name that predates the carrier vocabulary and describes position ("a child of some parent") rather than the carrier it is ("the Source carrier"). The corpus already asserts carrier: Source for every ChildField leaf (R299), so the rename closes the last gap between the model’s vocabulary and the code’s. (updated 2026-06-13)

  • R326 Render Mermaid diagrams on the published docs site: R316’s "model at a glance" section uses a Mermaid classDiagram. It renders on GitHub (where roadmap specs are actually read) but not on the AsciiDoctor docs site at graphitron.sikt.no, where it currently shows as a code listing. Future model specs will lean on diagrams; we want them rendered on the site too. (updated 2026-06-17)

  • R267 Replace deprecated-for-removal DataType.convert(Object) in NodeIdEncoder.decode<Type>: NodeIdEncoderClassGenerator.buildPerTypeDecode emits, for each NodeType, a decode<Type>(String) that builds a throwaway typed RecordN and populates it with rec.set(col, col.getDataType().convert(values[i])). DataType.convert(Object) is deprecated for removal in jOOQ 3.20 (it bypasses Configuration.converterProvider and misbehaves for user-defined types). The generator masks the resulting warning by stamping a class-wide @SuppressWarnings({"deprecation", "removal"}) on the whole NodeIdEncoder class — which only hides a future hard compile break when jOOQ actually removes the method. (updated 2026-06-01)

  • R402 Retire the ValueShape to synthetic CallSiteExtraction.InputBean round-trip in the bean-helper queue: Carved out of R256 (service-walker-substrate-absorption), whose deliverable 4 named two transitional-cruft cleanups. R256 landed the first (widening ConflictSite.site to a sealed MethodRef | ServiceMethodCall and retiring ContextArgumentClassifier.syntheticServiceMethodRef) and explicitly authorised splitting out this second, more opaque one if it grew. (updated 2026-06-30)

  • R448 Routine chains: ordering, binding, and corpus residue: Non-gating residue recorded during R435, none of it blocking the shipped surface: (updated 2026-07-08)

  • R447 Routine chains: remaining fetch-form breadth: R435 shipped order-significant @routine / @reference composition end to end for the core surface: every chain shape at root and child positions, the inline correlated multiset, and the @splitQuery batched keyed re-query on table-backed parents (batch key = the routine’s column-bound inputs). Four fetch-form extensions were left as typed Deferred landings whose planSlug points here; each falls on an existing model seam, so they slice independently: (updated 2026-07-08)

  • R454 Routine write result shapes: procedures, scalar/void routines, single-node Mutation @routine: R451 ships the table-valued write slice: @routine plus at least one @reference hop on a Mutation field, where the routine is a VOLATILE set-returning function and the response is the post-commit chain re-read. Everything whose result does not arrive as a table stays deferred here, with typed `Deferred`s pointing at this item’s planSlug: (updated 2026-07-09)

  • R170 Sakila execute-tier fixture for the Jakarta ValidationHandler channel (R94-blocked): Split out from R12 (error-handling-parity)'s "Test fixture updates for source-direct dispatch" bullet. R12 lifted ValidationHandler channel support and the Jakarta pre-execution validation step into the per-fetcher catch path. Execute-tier coverage is blocked on R94 (emit-input-records): the pre-step today calls validator.validate(env.getArgument(name)), which receives a Map<?, ?> (or a raw scalar); neither carries Jakarta annotations, so the validator returns no violations and the wire path is a no-op end-to-end. R94 emits each SDL input type as a graphitron-internal Java record under <outputPackage>.inputs, coerces the map at the fetcher boundary, and gives the validator a real annotated bean to inspect; once R94 lands this fixture becomes a one-line addition to the sakila execute-tier driver. (updated 2026-05-16)

  • R152 Scope @nodeId(typeName:) hover column lookup to the @node type’s @table: Hovers.formatNodeType (the hover popup for @nodeId(typeName: "X")) renders X’s NodeMetadata.keyColumns with each column’s graphqlType, but resolves the column type via columnGraphqlType(CompletionData, String) which linear-scans every table in the catalog case-insensitively and returns the first match. The lookup is not scoped to X’s @table-backing table, so when two tables in the catalog hold a column with the same name but different graphqlType projections (e.g. one mapped through a custom scalar via @scalarType, one not; or one nullable and one non-null), the hover renders whichever table the catalog enumerated first. Latent under Sakila (Sakila’s recurring column names map to identical jOOQ-generated graphql types) but a real bug for schemas where same-named columns diverge. The sibling columnHover for @field(name:) / @reference(key:) / @node(keyColumns:) at the directive’s own site already scopes via TypeContext.enclosingTypeDefinition + tableNameOf + catalog.getTable; the R100 hover diverges because the columns it renders belong to a different type than the one the cursor sits in. Fix: extend CompletionData.NodeMetadata to carry the @table name (CatalogBuilder.buildNodeMetadata already walks each @node-bearing GraphQLObjectType and can read the sibling @table directive in the same pass), then formatNodeType looks up catalog.getTable(meta.tableName()) and searches columns inside it, mirroring columnHover’s shape. Add a hover test with two tables that share a column name with diverging `graphqlType to pin the scoping. Surfaced during R100’s In Review → Done review. (updated 2026-05-13)

  • R282 Scope unknownForeignKeyRejection FK candidate hint to the structurally relevant FKs: Residual follow-up carved out of R259 (fk-key-hint-scope-and-namespace, shipped). R259 made the FK-key "did you mean" candidate hint both scoped (to the FKs touching the path source table) and namespace-aware (rendered in the SQL-constraint or jOOQ Java-constant TABLECONSTRAINT namespace the author typed) on the primary surface, BuildContext.parsePathElement via fkCandidateNames (BuildContext.java:897). The sibling surface, BuildContext.unknownForeignKeyRejection (BuildContext.java:1009), reached from the @reference(key:) / @nodeId synthesis miss path (call sites :1115, :1347, :1376, :1889), got the namespace half in the R259 close (mirrors in the attempt) but is still global: its candidate set is the whole catalog (allForeignKeySqlNames() / allForeignKeyConstantNames()), not scoped to the structurally relevant FKs. (updated 2026-06-08)

  • R277 Support @tableMethod under a table-bound NestingField: A plain object child of a @table parent, with no boundary directive (@reference/@splitQuery/FK), classifies as a ChildField.NestingField whose nested fields are bound to the parent’s table (FieldBuilder.classifyObjectReturnChildField, the TableBoundReturnType(elementTypeName, parentTableType.table()) arm). A @tableMethod field declared inside such a nesting is conceptually identical to a @tableMethod directly on the @table parent: it correlates the developer-returned table against the same parent row via the resolved FK’s source-side columns, and the nesting carries the same bound table. The generator’s projection-column collection already anticipates this (TypeClassGenerator.collectRequiredProjectionColumns has a TableMethodField arm and recurses into NestingField). (updated 2026-06-03)

  • R420 Support list-valued @nodeId+@reference on INSERT inputs (row fan-out): On the read side, [ID!] @nodeId filter leaves are a designed shape (IN predicate, see NodeIdReferenceFilterPipelineTest), but on the write side a list-valued node-id reference on an INSERT input has no semantics: the input maps to one inserted row with one FK slot, so a field like parentIds: [ID!]! @nodeId(typeName: "T") @reference(path: […​]) would have to mean row fan-out, one inserted row per decoded id, with the sibling scalar fields repeated across the fanned-out rows. Motivating case: FS’s OpprettUtdanningsspesifikasjonshierarkiInput, where callers today must repeat the whole input element per parent id. Deciding whether fan-out is wanted at all is part of this item; if yes, the INSERT emitter’s per-cell binding (TypeFetcherGenerator.buildInsertDecodeLocals / buildPerCellValueList) needs a list-aware decode-and-expand path, and the R419 build-time rejection gets lifted for the supported shape. Related: R57 covers list arity for the TranslatedFk join-translation case on the read side; this item is the DirectFk write path. (updated 2026-07-02)

  • R345 Surface schema parse failures as LSP red squiggles: Follow-on to R344 (which fixed the dev-log noise). A syntactically invalid schema is the canonical InvalidSchema case and the one diagnostic a developer can fix by rewriting their schema, yet today it produces no editor diagnostic: on parse failure the snapshot is demoted to Built.Previous and Diagnostics.validatorDiagnostics (Diagnostics.java:149-179) silences all squiggles (R139 freshness-aware silence: "a red squiggle the developer cannot fix by rewriting their schema is the noise we are trying to avoid"). A syntax error is precisely the carve-out that policy should admit. (updated 2026-06-19)

  • R461 Unify the four divergent SDL-field-to-Java-accessor resolution implementations behind one resolver: Resolving an SDL field name to a Java accessor on a backing class (bare name(), get<Name>(), is<Name>(), or a public field) is implemented four times with rules that disagree. The classification walk grounds a child field’s backing class using one set of rules while emission later resolves the same accessor under another, so the type validated at build time and the member invoked at runtime can differ. A fix applied to one copy silently does not propagate to the others. (updated 2026-07-10)

  • R257 UpdateRowsWalker raw-SDL substrate absorption: R246 shipped UpdateRowsWalker as a translator over the already-classified InputField permits (the four admitted column carriers ColumnField / CompositeColumnField / ColumnReferenceField / CompositeColumnReferenceField, reached via TableInputType.inputFields()) plus the jOOQ catalog, rather than re-deriving the input-field classification from raw SDL + classloader as the R246 spec’s ideal walk(GraphQLFieldDefinition, JooqCatalog) signature implied. This is the same blast-radius concession R238’s ServiceMethodCallWalker took (translating over a resolved MethodRef.Service rather than reflecting from scratch); re-deriving the @reference FK-join and @nodeId decode resolution inside the walker would have duplicated the substantial classifier in InputFieldResolver / EnumMappingResolver.buildLookupBindings. (updated 2026-05-29)

  • R467 Upgrade graphql-java 25.0 → 26.0: Bump graphql-java from 25.0 to 26.0 in the root pom.xml dependency-management block. Unlike the jOOQ 3.21 bump (R466) and the satellite catch-up (R465), this is a genuine breaking upgrade on two fronts, confirmed by a dry run. (updated 2026-07-10)

  • R466 Upgrade jOOQ 3.20.11 → 3.21.6: Adopt the jOOQ 3.21 line, bumping version.org.jooq in the root pom.xml (line 33) from 3.20.11 to 3.21.6. In jOOQ’s release cadence a minor bump (3.20 → 3.21) is the effective major boundary: it is where deprecated API is removed and code-generation output shape can change, so it is worth treating as a real upgrade even though the version delta looks small. 3.21 keeps the JDK-21 runtime floor 3.20 already established, so the "generated output targets Java 17" contract is undisturbed (the sakila-example module, which compiles generated code at --release 17, is the guard). (updated 2026-07-10)

  • R423 redaction reference id derives from OTel trace_id (via MDC) when present: Both redaction sites, the generated ErrorRouter.redactBody() (per-fetcher, inside graphql-java execution) and the hand-written GraphqlResource.execute() guard (R421, pre-execution seam faults), mint a fresh java.util.UUID.randomUUID() as the client-facing reference and log it alongside the real cause. That UUID correlates exactly one thing: the client’s Reference: <uuid> string and the single server ERROR log line carrying the same uuid. It is not the OpenTelemetry trace_id/span_id and is attached to no span, so in an OTel-instrumented deployment an operator cannot pivot from the client error into the trace backend; they grep logs by UUID instead (a two-hop path if the log line also carries trace_id via the consumer’s MDC instrumentation). This item makes the reference id derive from the ambient trace when one is present: read org.slf4j.MDC.get("trace_id") (SLF4J-only, the neutral bridge OTel’s log instrumentation already populates, so no OpenTelemetry dependency and no breach of the module’s vendor-neutral constraint, R416), use it as the reference when non-blank, and fall back to a random UUID when absent (no OTel, plain logging). The result: a one-hop pivot from a client error straight to the trace when OTel is running, graceful degradation to today’s behaviour when it is not. (updated 2026-07-02)

  • R394 roadmap-tool verify tripwires throw BuildFailure, not System.exit: The roadmap-tool verify-phase tripwires (README drift in Main.runVerify, front-matter validation in Main.validate, leaf-coverage drift/no-traces in LeafCoverageReport.run, and markdown-table findings in AdocMarkdownTableCheck.run) signal failure with System.exit(1) or a non-zero return that the dispatcher turns into System.exit. These checks run via exec-maven-plugin’s java goal, which executes in the Maven JVM, so System.exit terminates Maven directly: the contributor sees the friendly diagnostic line, then the shell prompt with $? = 1, no BUILD FAILURE banner and no [ERROR] line. The build looks like Maven crashed rather than failed a check. Replace those verify-mode tripwire exits with a thrown BuildFailure (a RuntimeException whose fillInStackTrace no-ops, since the failure surface is the printed diagnostic, not a Java stack) so exec-maven-plugin wraps it as a MojoExecutionException and produces the normal BUILD FAILURE summary. CLI/usage errors (usage(), argument errors, the create file-exists path) stay on System.exit, since those are dev errors outside any verify phase. This adapts the change from the stale claude/fix-maven-build-failure-yjL4J branch onto the current roadmap-tool structure, which has grown the check-adoc-tables verify tripwire since. (updated 2026-06-26)

Deferred

Items parked until a blocking concern is resolved or re-prioritised. Set deferred: false (or remove the field) to return an item to the active backlog.

Done

See Changelog for the historical record of shipped rewrite work. Plan files are deleted on Done; git history preserves them.