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 |
|---|---|---|---|---|
|
Dimensional model pivot: slots over cross-product permits |
Spec |
2026-07-04 |
|
|
The Graphitron data model |
Spec |
2026-07-05 |
|
|
Per-participant explicit join paths on multi-table interface/union child fields |
Ready |
2026-07-09 |
|
|
Dissolve the re-fetch (reentry) leaf fields: emit reentry by switching on the model |
Spec |
2026-07-04 |
|
|
Fold input/scalar/enum classification into the single classify-and-emit walk |
Spec |
2026-06-19 |
|
|
Model carrier arrival on the @service payload seat: one coherent list-payload shape verdict |
In Review |
2026-07-12 |
|
|
LSP-guided @reference path authoring |
Spec |
2026-06-25 |
|
|
Operation-divined tenant routing: tenant-column bindings select the per-tenant DataSource |
Spec |
2026-07-03 |
|
|
Source NodeId metadata from @node + catalog PK (inferred from |
Spec |
2026-06-02 |
|
|
Consolidate graphitron-lsp navigation, dispatch, and result-building |
In Progress |
2026-07-01 |
|
|
Faceted search on |
Ready |
2026-06-26 |
|
|
Surface database CHECK constraints as Jakarta validation rules |
Spec |
||
|
Centralize ResultType column-read emission for @record parents |
Spec |
2026-05-19 |
|
|
Consume R279’s ancestor-cardinality rider: fold true arrival and populate Source.OnlyChild |
Spec |
2026-07-10 |
|
|
DML payload positional input/output alignment |
Spec |
2026-06-15 |
|
|
Enumerate the capabilities graphitron delivers |
Spec |
||
|
How-to recipe and Sakila fixture for grouped collections via Field<Result<R>> @externalField + multiset |
Spec |
||
|
IntelliJ plugin wrapping graphitron:dev LSP |
Spec |
2026-05-21 |
|
|
Operation-driven test corpus, capability catalog, and runtime trace |
Spec |
Backlog
Architecture
-
R234Support jOOQ embedded and UDT records as non-table input backings: R222 collapsed the legacyJooqRecordInputTypearm by rejecting any non-TableRecordjOOQRecordsubclass at classification withRejection.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 noTableRefof their own. This item reintroduces the backing-class arm(s) those cases need — likelyEmbeddableRecord(fqClassName, embeddable: EmbeddableRef)andUDTRecord(fqClassName, udt: UDTRef)rather than a genericJooqRecordcatch-all, so each arm carries the structural metadata its downstream consumers actually want. Scope: extendBackingClasswith the new arm(s), wire the visitor’s runtime-shape classification to detect them, decide whether they participate inclassifiedFields(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) -
R411Wire-coercion cast guard for @condition and @externalField (R261 Slice 2): R261 Slice 1 landed the wire-coercion cast guard for the three@servicesites (A: input-bean scalar field, B:@servicescalar arg, E: input-bean enum field) plus the shared classify-time predicate (WireCoercionResolver), theWireCoercionErrorsealed family, and the enum-constant parity home (EnumMappingResolver.checkEnumConstants). This item is R261’s deferred Slice 2: the same defect on the two non-@servicearg-classification sites the Slice 1 spec named but left out: (updated 2026-07-01) (blocked by dimensional-model-pivot) -
R469Enable @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, andGraphitronConnectionInstrumentation.beginExecuteOperationrejects an execution with incremental support enabled outright (pinned byConnectionLifecycleExecutionTest). Enabling@defer/@streamunder 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@defersection; this item is its tracking home now that the R429 spec is deleted. (updated 2026-07-10) -
R468Oracle/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, RASCREATE_SESSION/ATTACH_SESSIONwith 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 thetest.db.urlseam 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) -
R171Fold InputType and TableInputType under sealed parent InputLikeType:GraphitronTypetoday permitsInputType(with four leaves:PojoInputType,JavaRecordInputType,JooqRecordInputType,JooqTableRecordInputType) andTableInputTypeas siblings. Any capability uniformly true of "things that come in as SDL input" (R94’sHasInputRecordShape, R98’sConstraintSet-attachment slot, any future input-side carrier) must be declared on five places instead of one, and a sixth input-like variant added toGraphitronType.permitswill not get a compile-time miss for the capability. Fold the two siblings under asealed 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) -
R337Input-side nesting-projection classification (NestingType mirror): Deferred in favor of R97 (consumer-derived-input-tables). This item proposed to fix the null-backedPojoInputTypemislabel for nested grouping inputs by adding a new per-typeGraphitronTypevariant (an input mirror of the outputNestingType). 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) -
R397Let bare-entity query fields host @error so decode and other client errors route through handlers:@errorison OBJECT: handlers are declared on a payload object that carries anerrorsfield, 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:WithErrorChannelis implemented byQueryServiceTableField,QueryServiceRecordField,QueryServicePolymorphicField, andQueryTableMethodTableField(WithErrorChannel.java:13names "root + child services, root + child@tableMethodfields"), andCatalogBuilderresolveserrorChannelName(f.errorChannel())for them. What has no error channel is a plain bare-entity fetch field (soknader: [Soknad!]): the sealedQueryFieldinterface does notextends WithErrorChannel(QueryField.java:25, unlikeMutationField.java:18), the table-fetch variants do not implement it, and their fetcher wraps work in a no-channel catch arm (TypeFetcherGeneratorredactCatchArm). So a client-facing error raised while fetching a bare-entity field cannot be mapped to a typed@errorpayload; the consumer’s only lever is the no-channel disposition. (updated 2026-06-29) -
R103Lift 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 whoseDataTypecarries adefaulted()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 emitsDSL.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. -
R231Emit 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: TextRatingwithenum TextRating { PG_13 @field(name: "PG-13") … }), graphitron’s field-emit lowers the GraphQL field type toStringin the generatedFieldDefinition(seeFilmType.java:36fortextRating). 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) -
R57FK-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 emitsBodyParam.In/Eq/RowIn/RowEqagainstjoinPath[0].sourceColumns()directly, no JOIN required. -
R72Slim 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), andreflectExternalField(~50 lines). Each owns its ownClass.forName+getDeclaredMethodsscaffolding, 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. -
R460Targeted 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 emitSELECT, 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 beVOLATILE/ may write) and@service(consumer Java that receives the pinned-connectionDSLContextand 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) -
R319Warn 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) -
R97Deprecate @table on input types; consumer-derived tables + argMapping grouping: The@tabledirective on input types declares "this input maps to columns of table X". The classifier consumes it to produceGraphitronType.TableInputType(TypeBuilder.buildTableInputTypeatTypeBuilder.java:686-718), and downstreamMutationInputResolver,EnumMappingResolver.buildLookupBindings,FieldBuilder(line ~697), andGraphitronSchemaValidator.validateTableInputTypeall switch on that variant. The directive is the structural signal that drives DML emit,@lookupKeyresolution, and condition-input column binding. -
R239Lift ColumnField.parentTable from emitter parameter to record component: Surfaced by R237 Phase 2 as a (b-cheap) structural-lift candidate. The classifier produces aChildField.ColumnFieldonly on a table-backed parent, but the parent table itself is currently threaded intoTypeFetcherGenerator.generateTypeSpecas a parameter rather than carried on theColumnFieldrecord. The switch arm atTypeFetcherGenerator.java:319readsparentTablefrom the parameter and throwsIllegalStateExceptionif null, treating a structurally-precluded reachability as a defensive guard. (updated 2026-06-26, created 2026-05-25) -
R192Mojo-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 generatedGraphitronContextimpl’sgetValidator(env)returnsValidation.buildDefaultValidatorFactory().getValidator(); consumers who need a customValidator(customConstraintValidatorimplementations, alternative providers, CDI integration) have no seam today and would have to reach for the legacyGraphitronContextinterface 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 overridingGraphitronContext.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) -
R323Multi-parent NestingField sharing: BatchKey leaves: Follow-up to R23 (which shipped theTableFieldarm of multi-parentNestingFieldsharing).GraphitronSchemaValidator.compareNestedFieldsShapestill rejects a plain-object nested type shared across multiple@tableparents when any shared leaf is a BatchKey carrier —SplitTableField,SplitLookupTableField,RecordTableField,RecordLookupTableField, or aRecord*MethodField— with the "not yet supported across multiple parents" deferral. Unlike the projectedTableFieldleaf 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’sLoaderRegistration/SourceKeyresolution 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) -
R46Multi-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) -
R123Parent-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. -
R66Widen 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 aRejectionconsumer, 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. -
R11DSLContexton@condition/@tableMethodmethods: Lift thereflectTableMethodgate. RequiresArgCallEmitterto walkparams()instead ofcallParams()so the injectedDSLContextlands 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) addedRecordN<…>source-shape support alongside the pre-existingRowN<…>shape on the@serviceclassifier path: developers freely choose either at the source declaration, and variant identity tracks shape —RowKeyed/MappedRowKeyedcarryRowNkeys;RecordKeyed/MappedRecordKeyedcarryRecordN. The only consumer-supplied surface left without that symmetry is@batchKeyLifter, whereBatchKeyLifterDirectiveResolverstill pins the lifter method’s return type toorg.jooq.Row1..Row22(BatchKeyLifterDirectiveResolver.java:266-273); aRecord1..Record22return is rejected today. This item brings the lifter API to the same Row-or-Record symmetry the source-shape path already has. -
R98Multi-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 JakartaConstraintMappingentries to those records derived from PostgreSQLCHECKconstraints. 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) -
R193Sealed UnresolvedParam classification for @service parameter rejection arms: The diagnostic-arm decision insideServiceCatalog.reflectServiceMethod(ServiceCatalog.java:258-329, thesourcesShape.isEmpty()block) is a chain of predicates over the unresolved Java parameter:classifySourcesType().isEmpty(), thenpName == null, thenparentPkColumns.isEmpty() && looksLikeSourcesShape(…), thendtoSourcesRejectionReason(…) != 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 soList<XRecord>at root falls through to the arg-mismatch diagnostic; R187 drops theparentPkColumns.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) -
R240Type-token threading on MethodRef.StaticOnly + ReturnTypeRef.TableBoundReturnType: Surfaced by R237 Phase 2 as a (b-relational) structural-lift candidate.ServiceCatalog.reflectTableMethodrejects developer methods whose return type is wider than the generated jOOQ table class via a strictClassName.equalscomparison, andTypeFetcherGenerator.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 neitherMethodRef.StaticOnlynorReturnTypeRef.TableBoundReturnTypecarries this relationship structurally. (updated 2026-06-26, created 2026-05-25) -
R172Audit: 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 tovalidator.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) -
R122Compound 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@servicemutation that orchestrates the inserts in Java, even when the relationships are entirely declarative from the SDL/jOOQ catalog perspective. (updated 2026-05-23) -
R427Relevance-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) -
R25Rebalance test pyramid: Shift new test investment from per-variant structural tests toward SDL-to-classification-to-emission pipeline tests keyed offgraphitron-fixtures. -
R174graphitron-javapoet: emit records, sealed/permits, package-info.java:graphitron-javapoetis forked from Square’s JavaPoet at a point that predates Java records (Java 14+) and sealed types (Java 17+). The emit framework supports fourTypeSpec.Kindvalues:CLASS,INTERFACE,ENUM,ANNOTATION. Records, sealed/permits clauses, andpackage-info.javafiles 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) -
R7DecomposeTypeFetcherGenerator:TypeFetcherGenerator.javais 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-shippedFieldBuilderdecomposition (R6, see [changelog.md](changelog.md)): a central generator that has accumulated coverage faster than its file shape can absorb. -
R145Cardinality 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: trueon@mutationis 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. ThemultiRow: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 insakila-exampleandGraphitronSchemaBuilderTestmigrate as part of this work. -
R207Audit 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-275saying plain inputs and@tableinputs share the same implicit-predicate behaviour) diverged from the implementation (FieldBuilder.java:1349passingnullforimplicitBodyParamson 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) -
R218Carry inference provenance on ParamSource.Arg so resolved bindings audit cleanly: R214’sServiceCatalog.inferBindingsByTypemutatesargByJavaNamesilently between the override-typo check and the per-parameter loop. The resultingParamSource.Arg(extraction, path)is structurally identical regardless of whether the binding came from an explicitargMapping, 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) -
R220Consolidate looksLikeSourcesShape, couldBeSourcesShape, and classifySourcesType into one predicate:ServiceCatalognow 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; addsTableRecordto the above, used by the inference gate to exclude SOURCES-shape params from candidate binding), andclassifySourcesType(gated byparentPkColumns.isEmpty()and produces a typedSourcesShaperesult). 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) -
R117Graphitron 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. -
R304Reify @error PayloadAccessor errors fetcher into a named method: R303 reified every datafetcher onto a named<Type>Fetchersmethod except one: the@error-type errors field on theTransport.PayloadAccessorarm, which still registers graphql-java’sPropertyDataFetcher.fetching(name)(a runtime reflective property read off the parent payload). (updated 2026-06-14) -
R219Unify arity-unique and type-unique inference under a single JavaTypeKey-counted rule: R214’sinferBindingsByTypeshipped 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 (mapToJavaTypeNamereturnsnullfor them and they’re dropped fromslotsByType). 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) -
R221Validator walks PlainInputArg.fields() for UnboundField rejection: R215’s validator-side check onInputField.UnboundField(per the spec’s §Validator rules:condition.isPresent() && !condition.get().override()→ reject at the directive’s source location) only walksGraphitronType.TableInputType.inputFields()viaGraphitronSchemaValidator.validateTableInputType+validateInputFieldRecursive. Truly plain input types (GraphitronType.InputTypepermits —JavaRecordInputType,PojoInputType,JooqRecordInputType,JooqTableRecordInputType) carry no classifiedInputFieldrecords on the type; their fields classify at consumer time onArgumentRef.InputTypeArg.PlainInputArg.fields(). The validator’s whole-schema walk has no view intoPlainInputArg, so plain-inputUnboundField + @condition(override:false)shapes escape the validator-mirrors-classifier rule. R215’s acceptance test #5 (r215_validatorRejectsOverrideFalseOnNonBindingField) was written against a@tableinput (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
-
R263Add a typeName-first decode-helper entry point so resolveDecodeHelperForTable is not a misuse trap:BuildContext.resolveDecodeHelperForTable(BuildContext.java:2111-2136) resolves thedecode<TypeName>suffix fromfindGraphQLTypeForTable(sqlTableName)(singular,:1992,:2118) and consults itsfallbackTypeNameOrTypeIdargument 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@tabletype backs the table; when several object types share that table the method yieldsdecode<firstTypeForTable>, notdecode<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) -
R360Retire 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 columnjavaType, a@servicesignature type, an accessor return type) whose Java type is the backing. This is the same producer-reflection that retired@record. So an authored@enumvalue adds no information graphitron cannot derive and can only contradict the inferred truth, a pure misconfiguration surface. Retire it the way@record/@notGenerated/@multitableReferenceare 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 (theEnumBackingroll-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 aString(varchar) or numeric (integer) column, so the inference generalizes overbackingType, not just jOOQ-generated enum classes. (updated 2026-06-23) -
R133Flip leaf-coverage profile activation to opt-in: Theleaf-coverageprofile ingraphitron-rewrite/pom.xmlis activated by negation (<name>!leaf-coverage.skip</name>), so every default contributormvn verifytruncatestarget/leaf-coverage.jsonlinprocess-test-resourcesand threads agraphitron.classification.tracesystem property into every surefire/failsafe. The traces are only consumed byroadmap-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. -
R359Guard 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, likeTableRef.tableName(), a case-preserved verbatim identity string, while the jOOQ catalog’sfindColumnlookup (JooqCatalog.java:813) is the already-case-insensitive layer (the column analogue offindTable). So the same structural drift exists: one live case-sensitive.sqlName().equals((GraphitronSchemaValidator.java:883,!rcf.column().sqlName().equals(ocf.column().sqlName())) sits alongside sixequalsIgnoreCasecomparison sites (FieldBuilder.java,TypeBuilder.java,BuildContext.java,NodeIdLeafResolver.java×3). R358 scoped itself totableNameand explicitly excluded this; the proportionate fix mirrors R358’s Phase 2: asameColumn(…)-style predicate onColumnRefplus 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) -
R54Rename @externalField (parallel-support, deprecation, migration):@externalFieldlifted toIMPLEMENTED_LEAVESend-to-end incomputed-field-with-reference(R48, shipped; see [changelog.md](changelog.md)). The directive’s name is the surviving historical artefact: it predates theChildField.ComputedFieldmodel variant and reads as "field resolved by external code" rather than the narrower behaviour the lift settled on (aField<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. -
R27Retire@nodeIdandIdReferenceFieldsynthesis 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) -
R51Split PropertyField/RecordField on parent-kind instead of nullable column:ChildField.PropertyFieldandChildField.RecordFieldeach carry bothcolumnName: Stringandcolumn: ColumnRef, withcolumnnullable depending on the parent type: non-null when the parent is aJooqTableRecordTypewith a resolvable column, null forJooqRecordType/JavaRecordType/PojoResultTypeparents. The single record straddles two parent kinds via an Optional component, leavingcolumnNameas the only carrier of the SDL string whencolumnis 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-nullColumnRef, one for non-table-backed parents carrying just the SDL string), not one record with a nullable component. Split surfaced during R50’scolumnNamecleanup onChildField.ColumnField/ColumnReferenceField, where the table-backed-only invariant let those carriers retirecolumnNameoutright; this item carries the same rigour toPropertyFieldandRecordField. -
R235Tidy @reference path-element surface: separate join-shape from WHERE-filter: The legacyReferenceElement { table, key, condition }directive surface combines three roles in one input object:key:andtable:andcondition:(without companions) name the join shape;condition:combined withkey:ortable:names a WHERE-filter that folds onto the FkJoin’swhereFilter. 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) -
R17Annotated 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 greppinggraphitron-test/target/. -
R76Emit per-participant fieldsJoin and orderBy; replace SelectJoinStep mutation in interface fetchers:TypeFetcherGenerator.buildQueryTableInterfaceFieldFetcherandbuildTableInterfaceFieldFetcheremit dynamic jOOQ queries by declaring aSelectJoinStep<Record> steplocal and reassigning it insideif (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 viaDSL.noTable()/DSL.noCondition(), which jOOQ erases at render time. The step-mutation form also centralises join construction inQueryFetchers, breaking symmetry with the existing per-type-class$fields(…)helper (which already gates SELECT entries by selection set on the participant type). -
R417Reconcile sakila-example README app-section with R399 (dead GraphqlEngine/GraphqlResource/AppContext links):graphitron-sakila-example/README.mdstill describes the runtime under a "Runnable reference (the app)" section (roughly lines 17-27) as three example-owned files:GraphqlEngine.java,GraphqlResource.java, andAppContext.java. R399 extracted all of that intographitron-jakarta-rest; the example now ships a singleSakilaGraphitronApplicationadapter (aGraphitronApplicationSPI implementation) and depends on the library for the/graphqlresource, the engine, status-code semantics, the/schemaendpoint, 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) -
R47Short class-name resolution for@serviceand@externalField(legacy parity):ServiceCatalog.reflectServiceMethodcurrently callsClass.forName(className)directly, forcing an FQN. Existing schemas carry short class names likeclassName: "PersonService"and rely on the legacy Mojo’sexternalReferenceImportslist 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. -
R35Class-level Javadoc andpackage-info.javasweep: A reader landing onFieldBuilder.java(2 172 lines) orTypeFetcherGenerator.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 zeropackage-info.javafiles, which is the IDE-native place for "what is in this package" blurbs. -
R116Cover composite-key Row2 path-keyed @sourceRow classification: R110 shipped@sourceRowwithRow2..Row22arity admitted by the resolver: the per-position type loop inSourceRowDirectiveResolveriterates the lifter’sRowNtype arguments without special-casing arity 1, and the leaf-PK arm constructsLifterLeafKeyedover whatever the leaf’sTableRef.primaryKeyColumns()returns. The existingSourceRowClassificationCasetest enum exercises Row2 only on the rejection path (LEAF_PK_ARITY_MISMATCHagainstinventory.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 ingraphitron-rewrite/graphitron/src/test/…), not in resolver / emitter code. -
R120Drop or wire FkJoin.alias dead storage:BuildContext.synthesizeFkJoin(BuildContext.java:694) populatesFkJoin.aliasasfieldName + "_" + stepIndex(e.g."language_0") while resolving a@referencepath. No code reads it:JoinPathEmitter.generateAliases(JoinPathEmitter.java:41) derives its own per-hop aliases from the target table’sjavaClassName()+ 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 siblingConditionJoin.aliasslot. -
R126Scrub residual BatchKey.X references from sakila-service / sakila-example prose: R38’s Phase 3 follow-up ("Stale-prose scrub", commit5d82380) 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 exampleschema.graphqlsdescription comments still mention deletedBatchKey.Xpermits (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. -
R10Drop the assembled-schema rebuild in favour of per-variant graphql-java forms: Phase 5 of [firstclass-connection-types](firstclass-connection-types.md) rebuilds the assembledGraphQLSchemaviaSchemaTransformerso directive-driven@asConnectioncarriers 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 inGraphitronSchema.build()). -
R24NodeIdReferenceFieldJOIN-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 producesChildField.ColumnReferenceField/CompositeColumnReferenceFieldwithcompaction = NodeIdEncodeKeysand a resolvedjoinPath). What did not ship is the matching emitter:FetcherEmitter#dataFetcherValuecarries runtimeUnsupportedOperationExceptionstubs 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. -
R34sis-graphql-spec migration to graphitron-rewrite: Track the consumer-side schema work needed to bringsis-graphql-speccleanly 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). -
R280Typed 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 itsOptional<CodeBlock>deferral should not lose track of. (updated 2026-06-05) -
R168Sub-agent classifier for blast-radius effort (Low/Medium/High) at Spec stage: The roadmap roll-up sorts bypriority:but carries no signal about how big a piece of work each Active item is. A reader scanning the table cannot tell whetherReadymeans "one afternoon" or "a multi-phase lift across four modules", and the author settingpriority:is making that judgement implicitly without surfacing it. Add aneffort:front-matter field with valuesLow | Medium | Highdefined 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 inroadmap/README.mdand as an attribute in the per-plan AsciiDoc page; gate the validator so aneffort:value on aBacklogitem is a hard error. (updated 2026-05-16) -
R85Emit 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 producesCallSiteExtraction.ContextArgfor context-bound filter parameters (MethodRef.java:111, exercised atGraphitronSchemaBuilderTest.java:2768);ArgCallEmitter.buildArgExtraction’s `ContextArgarm emitsgraphitronContext(env).getContextArgument(env, …); the call lands in<RootType>Conditions.<field>Condition()(viaQueryConditionsGenerator) or in<TypeName>.$fields()(viaInlineTableFieldEmitter/InlineLookupTableFieldEmitter). Neither host class emits agraphitronContexthelper, so the generated source fails atmvn compile -pl :graphitron-sakila-examplewith "cannot find symbol: graphitronContext". The bug is currently latent because no fixture ingraphitron-sakila-exampleorgraphitron-fixtures-codegenusescontextArgumentson@condition. -
R208Retire the @asConnection(connectionName:) deprecated argument:@asConnection(connectionName:)is deprecated indirectives.graphqls(the SDL@deprecatedmarker landed alongside R93’sSdlActionmigration registry; seedirectives.graphqls:243) but still functional:ConnectionPromoter.resolveConnectionNamehonours an explicit override when present and falls back to the<ParentType><FieldName>Connectionderivation 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) -
R245Wire @condition through to mutation WHERE (emit half + new placements):@conditionon mutations is half-built today:MutationInputResolver.javaadmits 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@conditionon a non-@tablemutation argument is rejected outright (line 446). Input-field-level@conditionwithoutoverride: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
-
R136Execution-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}inNodeIdPipelineTest), which assertsliftedSourceColumnsis permuted into@node.keyColumnsorder on the resolver’sDirectFkcarrier. The end-to-end SQL correctness — that the emittedBodyParam.RowEqagainstliftedSourceColumnsactually matches the right rows when joined against decoded NodeId values — is not exercised by an execution-tier test in this repo. -
R135Multi-hop @nodeId pipeline test for FK-target/NodeType-keyColumns permutation: R131’s permutation relaxation inNodeIdLeafResolver.resolveaccepts set-equality between the terminal hop’s target columns and the NodeType’s@node(keyColumns:), then permutesliftedSourceColumnsinto NodeType-keyColumns order before constructingResolved.FkTarget.DirectFk. The pipeline-tier test pinning this lands on the single-hopreordered_pk_parentfixture (InputFieldFkTargetNodeIdCase.FK_TARGET_REORDERED_KEY_PERMUTATION_DIRECT_FK{,_SINGULAR}). -
R181Validate @order/@defaultOrder: empty directive and @index coexistence: A real user report (paraphrased) crashed the schema build: (updated 2026-05-20, created 2026-05-19) -
R107Classify leaf mentions in inference-axis-coverage report:LeafCoverageReport.parseMentions(R104) joins each sealed leaf simple-name against every roadmap*.mdbody via a\b<simpleName>\bregex. The match is undifferentiated: backticked code spans, code-fenced blocks, and bare prose mentions all collapse into the sameRoadmapcell. Two consequences. First, every roadmap edit that names a leaf in any form driftsinference-axis-coverage.adocand trips theverify-leaf-coverage-reportCI gate, which is the regen-friction tax R104 deferred. Second, a reviewer reading the column has no way to sanity-check a match —FieldagainstFieldTypeis excluded by\b, but a phrase like "the field type" cannot be told apart from a deliberatesymbol reference without re-reading the source spec body.Field -
R419Reject 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:NodeIdLeafResolveris arity-agnostic,BuildContext.classifyInputFieldjust bakeslist=trueinto theColumnReferenceField/CompositeColumnReferenceFieldcarrier, andMutationInputResolver.admitMutationInputFieldsadmits reference carriers for INSERT unconditionally (only theNestingFieldarm rejects lists). The generated code compiles but hardcodes single-value assumptions (instanceof Stringdecode guard,.value1()bind inTypeFetcherGenerator), so any non-empty list value throwsGraphitronClientException"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), alist()guard inadmitMutationInputFieldsalongside the existing nesting-field list rejection should turn this into a clear build-time schema error. (updated 2026-07-02)
Other
-
R236BuildContext 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 fromcatalog.columnSqlNamesOf(resolvedTable.tableName())whereresolvedTableis the path-origin enclosing input’s@table, not the path’s terminal table. (updated 2026-05-23) -
R373Capture test stdout/stderr to per-class files via Surefire redirectTestOutputToFile: Several tests emit info/warn/error logs (SLF4J + LogbackConsoleAppender→System.out) and stack traces during a normalmvn installrun, 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, butredirectTestOutputToFile=truecaptures each test class’s stdout/stderr intotarget/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) -
R432Collapse SplitTableField and RecordTableField into one source-gated leaf: The R333 beachhead: collapseChildField.SplitTableFieldandChildField.RecordTableFieldinto one leaf gated on the source fact. Thread A measured the two component-identical (11 shared components, bothTableTargetField+BatchKeyField; onlyemitsSingleRecordPerKey()andsourceShape()differ), and both child sides already lower to the sameload<X>rows-method and fetcher; Split’s only extra, the parent-key projection, already lands viacollectRequiredProjectionColumnsin 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) -
R297Collapse the shareable boolean on ConnectionType/EdgeType/PageInfoType; read federation flags off schemaType():ConnectionType/EdgeType/PageInfoTypeeach carry ashareableboolean component alongside theirschemaType()GraphQLObjectType. After R295, federation@tagpropagation onto these synthesised types is driven entirely offschemaType()(the applied directives ride on the schema form; notagsrecord component, per "Model metadata over parallel type systems"). Theshareableboolean is now the asymmetric survivor: it is a second representation of@shareable, redundant with the schema form for emission (the synthesisedschemaType()already carries the directive, and no emitter readsshareable()), and its only consumer is thepageInfoShareable |=fold insideConnectionPromoter.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) -
R289Correct 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@nodetype "keeps it out of_Entity`". That is false for the pinned federation-jvm version: `Federation.transforminjects every@key-bearing type into the_Entityunion regardless ofresolvable:. The behaviour is empirically pinned byFederationBuildSmokeTest.resultEntityUnionContainsAllFixtureEntities, which asserts both the table-boundLanguagestub and (since R286) the non-table-boundFilmRefStub, both@key(resolvable: false), are present in the served_Entityunion.resolvable: falsedoes 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 whatresolvable: falseactually does (composer routing, not union membership), and auditKeyNodeSynthesiser: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) -
R431Decompose SourceKey onto the model’s facts:SourceKeyis(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) -
R393Disambiguate 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@tableand its base-only inherited fields carry@referenceback 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) -
R209FieldRegistry classify-input trace loses typed Rejection payload:FieldRegistry.classifyInputatgraphitron/src/main/java/no/sikt/graphitron/rewrite/FieldRegistry.java:108-110emits the trace record for anInputFieldResolution.Unresolvedoutcome by defaulting toRejectionKind.AUTHOR_ERRORwithu.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 structuredattempt + candidatespayload they would otherwise consume on a column-missUnresolved, and on non-column-missUnresolvedthey get anAUTHOR_ERRORlabel that may not match the actual rejection kind. R205 closed the gap one layer up (InputFieldResolver.resolvenow lifts to typedRejection.unknownColumn/Rejection.structural); the corresponding lift insideFieldRegistry.classifyInputwas flagged in the R205 self-review and deferred. Two design forks worth thinking through during Spec: (a) widenInputFieldResolution.Unresolvedto carry aRejection(touches every Unresolved construction site in the classifier; some sites lack catalog/candidates context to buildunknownColumn), or (b) threadTableRef rtintoFieldRegistry.classifyInputand lift toRejectionthere. (a) keeps the lift co-located with classification; (b) keepsUnresolvedtransient by design. Either way the deliverable is removing theRejectionKind.AUTHOR_ERRORdefault arm and emittingRejectionKind.of(rejection)consistently withtraceOutputatFieldRegistry.java:127-130. (updated 2026-05-21) -
R298First-client contract-composition check: do type-level @tag-only synthesised connection types satisfy a real federation contract?: R295 propagates the carrier field’s federation@tagapplications 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:TagAppliertags 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 inConnectionFederationTagPipelineTestproves 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) -
R334Generated @condition arg extraction is an unreadable nested-ternary one-liner: Every generated@conditionargument 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-targetEXISTSmulti-line, but did not touch the per-argument extraction, which is cross-cutting: it lives inArgCallEmitter.buildArgExtractionand feeds every WHERE-emitting site (theQueryConditionsshim plus the inline / lookup / split fetcher emitters). The fix likely extracts each argument into a named local (a clearly-typedvar <name> = …) before the call, or routes through a small generated helper, so the call site reads asConditions.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) -
R118Graphitron 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 ingraphitron: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. -
R201Honor @field(name:) in @error payload construction shape resolution:FieldBuilder.resolvePayloadConstructionShape(FieldBuilder.java:506-609) picks an@errorpayload class’s construction shape, then the emitter (catch-armpayloadFactoryLambdainTypeFetcherGenerator, and the validator pre-step’sdeclareEarlyPayloadFromErrors) 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-591matchesset<UcFirst(sdlFieldName)>onpayloadCls.getMethods()via the Java-bean conversion injavaBeanSetterName; the classifier’s existing invariant at:498-505pins "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-535picks the single ctor whose parameter count equalssdlFieldNames.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 onSetterBinding; 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-505to 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) -
R202Honor @field(name:) in @error type extra-field accessor matching against handler source class:FieldBuilder.checkErrorTypeSourceAccessors(FieldBuilder.java:2281-2316) verifies that each@errorobject type’s extra fields (everything exceptpath/message) can be populated from the handler’s source class (the exception class for aGENERIChandler, theDataAccessException-shaped source for aDATABASEhandler, etc.). For each(sdlField, sourceClass)pair it callsClassAccessorResolver.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 exposinggetErrorCode()mapped to an SDL field namedcode, or a Norwegian-named accessor under an English SDL — the resolver returnsRejectedand 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 readDIR_FIELDon each extrasdlFieldat:2298-2305and pass the directive value (when present) as the second arg toClassAccessorResolver.resolveinstead ofsdlField.getName(). The directive’s docstring already covers this site under the "underlying-binding target" reading; the docstring update tracked separately should mention@errorextra 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) -
R69Implement @experimental_constructType: The@experimental_constructType(selection: "…")directive is declared indirectives.graphqlsand stripped from the emitted schema bySchemaDirectiveRegistry, but no classifier, model carrier, or emitter exists for it yet. -
R288Inline TableInterfaceField and TableMethodField children (currently N+1): A child@tablefield 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 ordinaryChildField.TableField(InlineTableFieldEmitter). It does not. Both leaves get a generated synchronous fetcher method (TypeFetcherGenerator.buildTableInterfaceFieldFetcher,buildChildTableMethodFetcher) that runs its own per-parentdsl.select(…).from(…).where(parent-correlation).fetch()againstenv.getSource(). There is noSplitRowsMethodEmitterand 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) -
R430LSP 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 MCPdiagnosticstool, and the LSP publishing them against the generated file’s URI (best-effort, so an editor with that generated.javaopen shows the javac error inline). R410 shipped the first two; the diagnostics already land onWorkspace.compileDiagnostics()(in the LSP module) after every round, but no LSPtextDocument/publishDiagnosticsis emitted for them. Close the gap: on eachsetCompileDiagnosticsswap, publish the round’s error diagnostics against the generated-file URIs (resolvingCompileDiagnostic.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) -
R382Lower orderBy onto multitable-interface/union queries: A root query field returning a multitable interface or union (QueryField.QueryInterfaceField/QueryField.QueryUnionField, and the@asConnectionvariant) cannot carry a user-specified ordering.operation()hardcodesnew OrderBySpec.None()for both arms, and the emitter orders results solely by the syntheticsortkey (the participant PK). A consumer asking for a specific order gets PK order regardless. (updated 2026-06-25) -
R387Migrate TypeConditionsGeneratorTest off code-string assertions on generated method bodies:TypeConditionsGeneratorTestasserts on generated condition-method bodies viamethod.code().toString()+ AssertJcontains(…)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 theRemoteColumnPredicate/ correlated-EXISTSarm; 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-exampleagainst 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/MethodSpecstructure — theparamType/ 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. theEXISTScorrelation 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) -
R462Model the nested fetcher own outgoing per-field precise edges in CompileDependencyGraphBuilder: R459 registered the<Type>Fetchersnode 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 nestedSplitTableField’s DataLoader methods reference the target type’s projection class; a nested composite / `@nodeIdread referencesNodeIdEncoder.CompileDependencyGraphBuilder.addFieldEdgesnever sees these fields because they are absent fromschema.fields()(andschema.fieldsOf(nestedType)is empty for a coordinate-less nesting type). TheTypeSpecReferenceWalkcompleteness oracle will flag any such edge as a superset gap once a harness fixture exercises it. (updated 2026-07-10) -
R252Multi-file federation fixture coverage for schema.graphqls emission: R247’sSchemaSdlEmitterwritestarget/generated-resources/…/schema.graphqlsby callingServiceSDLPrinter.generateServiceSDLV2(assembled)on the codegen-time schema. The only pipeline-tier coverage isFederationBuildSmokeTestagainst sakila’s single-file federated fixture (federated-schema.graphqls, which carriesextend schema @link(…)and types in the same file). Real-world consumer schemas often split across many files — one carryingextend schema @link(…), others carrying types only, no explicitschema { … }block anywhere — and that shape goes through a different code path inRewriteSchemaLoader(MultiSourceReaderconcatenates streams, singleParser.parseDocument, singleSchemaParser.buildRegistry). (updated 2026-05-27) -
R249Nested @argMapping syntax via GraphQLSelectionParser: Today’s@argMappingparses as comma-separatedjavaParam: <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) -
R412Nested backing class emits $-qualified names at the no-Class-in-hand emit sites (backingClassOf, recordColumnReadArgs, FetcherEmitter, ChildField): R370 fixed theClassName.bestGuess(binaryName)nested-class defect (a nested backing class emits the non-compilingOuter$Nestedinstead of the JLS-legalOuter.Nested) at the four sites that had a structurally-correct name already in hand: the twoAccessorRefproducers, the@servicereturn-type validator (checkServiceReturnMatchesPayload), and the@servicefetcher return type (computeServiceRecordReturnType). The samebestGuess-over-fqClassName()hazard recurs at a family of emit sites that hold no reflectedClass<?>and no captured structuralTypeName, so they cannot take R370’s one-for-one call swap: (updated 2026-07-01) -
R274OutcomeType carries its success projection so the nullability invariant lives on the carrier: R244’s spec framesOutcomeTypeas a carrier whosesuccessProjectionholds "the non-errors data fields, all nullable (enforced)" so that "possessing anOutcomeTypeis 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: atFieldBuilder.resolveServiceOutcomeChannel(FieldBuilder.java:~2106) theOutcomeTypeis constructed withsuccessProjection = List.of(), and the nullable-success-projection invariant (NonNullableSuccessProjectionField) is instead enforced by an inline loop overpayloadObj.getFieldDefinitions()before construction. So the invariant lives in a loop, not the type, and theOutcomeTypevalue 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) -
R213Plain-input field rejections attributed to consumer field, losing input-field source location: When a plain (non-@table) input type carries a broken@conditiondirective (parameter-binding mismatch, reflection failure, etc.) or an unresolvable column, the failure surfaces as anUnclassifiedFieldon the consuming field (e.g.Query.opptak), not on the input type’s offending field (e.g.OpptakFilterInput.opptaksNavn). The reportedSourceLocationis 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) -
R278Polymorphic type classification: sealed union-type variants over ParticipantRef: Resurrected. This item originally proposed classifying polymorphic types (interface/union) "in field context" by enriching theParticipantRefparticipant-list model. It was briefly absorbed into R279 (field-first-classification-driver) as a set ofParticipantReftweaks, 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) -
R443Post-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) -
R348Regenerate and guard the generated supported-schema-shapes migration doc against drift:docs/manual/generated/supported-schema-shapes.adocis a generated migration fragment (included bydocs/manual/how-to/migrating-from-legacy.adoc) produced bygraphitron-roadmap-tool leaf-coverage --mode=migration(LeafCoverageReport.run, migration branch). Like its siblingsupported-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 runsleaf-coverage --mode=migration. The onlyleaf-coveragestep in CI (rewrite-build.yml"Regenerate leaf-coverage report") runsleaf-coverage graphitron-rewritewith no--mode=migration, regenerating the internalinference-axis-coverage.adocreport, 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) -
R302Rename 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:Query→QueryField,Mutation→MutationField, andSource→SourceField. The first two already match their carrier; the third is still calledChildField, 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 assertscarrier: Sourcefor everyChildFieldleaf (R299), so the rename closes the last gap between the model’s vocabulary and the code’s. (updated 2026-06-13) -
R326Render Mermaid diagrams on the published docs site: R316’s "model at a glance" section uses a MermaidclassDiagram. 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) -
R267Replace deprecated-for-removal DataType.convert(Object) in NodeIdEncoder.decode<Type>:NodeIdEncoderClassGenerator.buildPerTypeDecodeemits, for each NodeType, adecode<Type>(String)that builds a throwaway typedRecordNand populates it withrec.set(col, col.getDataType().convert(values[i])).DataType.convert(Object)is deprecated for removal in jOOQ 3.20 (it bypassesConfiguration.converterProviderand misbehaves for user-defined types). The generator masks the resulting warning by stamping a class-wide@SuppressWarnings({"deprecation", "removal"})on the wholeNodeIdEncoderclass — which only hides a future hard compile break when jOOQ actually removes the method. (updated 2026-06-01) -
R402Retire 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 (wideningConflictSite.siteto a sealedMethodRef | ServiceMethodCalland retiringContextArgumentClassifier.syntheticServiceMethodRef) and explicitly authorised splitting out this second, more opaque one if it grew. (updated 2026-06-30) -
R448Routine chains: ordering, binding, and corpus residue: Non-gating residue recorded during R435, none of it blocking the shipped surface: (updated 2026-07-08) -
R447Routine chains: remaining fetch-form breadth: R435 shipped order-significant@routine/@referencecomposition end to end for the core surface: every chain shape at root and child positions, the inline correlated multiset, and the@splitQuerybatched keyed re-query on table-backed parents (batch key = the routine’s column-bound inputs). Four fetch-form extensions were left as typedDeferredlandings whoseplanSlugpoints here; each falls on an existing model seam, so they slice independently: (updated 2026-07-08) -
R454Routine write result shapes: procedures, scalar/void routines, single-node Mutation @routine: R451 ships the table-valued write slice:@routineplus at least one@referencehop on a Mutation field, where the routine is aVOLATILEset-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) -
R170Sakila 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 liftedValidationHandlerchannel 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 callsvalidator.validate(env.getArgument(name)), which receives aMap<?, ?>(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 SDLinputtype 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) -
R152Scope @nodeId(typeName:) hover column lookup to the @node type’s @table:Hovers.formatNodeType(the hover popup for@nodeId(typeName: "X")) renders X’sNodeMetadata.keyColumnswith each column’sgraphqlType, but resolves the column type viacolumnGraphqlType(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 differentgraphqlTypeprojections (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 siblingcolumnHoverfor@field(name:)/@reference(key:)/@node(keyColumns:)at the directive’s own site already scopes viaTypeContext.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: extendCompletionData.NodeMetadatato carry the@tablename (CatalogBuilder.buildNodeMetadataalready walks each@node-bearingGraphQLObjectTypeand can read the sibling@tabledirective in the same pass), thenformatNodeTypelooks upcatalog.getTable(meta.tableName())and searches columns inside it, mirroringcolumnHover’s shape. Add a hover test with two tables that share a column name with diverging `graphqlTypeto pin the scoping. Surfaced during R100’s In Review → Done review. (updated 2026-05-13) -
R282Scope 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-constantTABLECONSTRAINTnamespace the author typed) on the primary surface,BuildContext.parsePathElementviafkCandidateNames(BuildContext.java:897). The sibling surface,BuildContext.unknownForeignKeyRejection(BuildContext.java:1009), reached from the@reference(key:)/@nodeIdsynthesis miss path (call sites:1115,:1347,:1376,:1889), got the namespace half in the R259 close (mirrorsin 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) -
R277Support @tableMethod under a table-bound NestingField: A plain object child of a@tableparent, with no boundary directive (@reference/@splitQuery/FK), classifies as aChildField.NestingFieldwhose nested fields are bound to the parent’s table (FieldBuilder.classifyObjectReturnChildField, theTableBoundReturnType(elementTypeName, parentTableType.table())arm). A@tableMethodfield declared inside such a nesting is conceptually identical to a@tableMethoddirectly on the@tableparent: 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.collectRequiredProjectionColumnshas aTableMethodFieldarm and recurses intoNestingField). (updated 2026-06-03) -
R420Support list-valued @nodeId+@reference on INSERT inputs (row fan-out): On the read side,[ID!] @nodeIdfilter leaves are a designed shape (IN predicate, seeNodeIdReferenceFilterPipelineTest), 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 likeparentIds: [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’sOpprettUtdanningsspesifikasjonshierarkiInput, 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 theTranslatedFkjoin-translation case on the read side; this item is theDirectFkwrite path. (updated 2026-07-02) -
R345Surface schema parse failures as LSP red squiggles: Follow-on to R344 (which fixed the dev-log noise). A syntactically invalid schema is the canonicalInvalidSchemacase 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 toBuilt.PreviousandDiagnostics.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) -
R461Unify 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 (barename(),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) -
R257UpdateRowsWalker raw-SDL substrate absorption: R246 shippedUpdateRowsWalkeras a translator over the already-classifiedInputFieldpermits (the four admitted column carriersColumnField/CompositeColumnField/ColumnReferenceField/CompositeColumnReferenceField, reached viaTableInputType.inputFields()) plus the jOOQ catalog, rather than re-deriving the input-field classification from raw SDL + classloader as the R246 spec’s idealwalk(GraphQLFieldDefinition, JooqCatalog)signature implied. This is the same blast-radius concession R238’sServiceMethodCallWalkertook (translating over a resolvedMethodRef.Servicerather than reflecting from scratch); re-deriving the@referenceFK-join and@nodeIddecode resolution inside the walker would have duplicated the substantial classifier inInputFieldResolver/EnumMappingResolver.buildLookupBindings. (updated 2026-05-29) -
R467Upgrade graphql-java 25.0 → 26.0: Bumpgraphql-javafrom 25.0 to 26.0 in the rootpom.xmldependency-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) -
R466Upgrade jOOQ 3.20.11 → 3.21.6: Adopt the jOOQ 3.21 line, bumpingversion.org.jooqin the rootpom.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) -
R423redaction reference id derives from OTel trace_id (via MDC) when present: Both redaction sites, the generatedErrorRouter.redactBody()(per-fetcher, inside graphql-java execution) and the hand-writtenGraphqlResource.execute()guard (R421, pre-execution seam faults), mint a freshjava.util.UUID.randomUUID()as the client-facing reference and log it alongside the real cause. That UUID correlates exactly one thing: the client’sReference: <uuid>string and the single serverERRORlog line carrying the same uuid. It is not the OpenTelemetrytrace_id/span_idand 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 carriestrace_idvia the consumer’s MDC instrumentation). This item makes the reference id derive from the ambient trace when one is present: readorg.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) -
R394roadmap-tool verify tripwires throw BuildFailure, not System.exit: The roadmap-tool verify-phase tripwires (README drift inMain.runVerify, front-matter validation inMain.validate, leaf-coverage drift/no-traces inLeafCoverageReport.run, and markdown-table findings inAdocMarkdownTableCheck.run) signal failure withSystem.exit(1)or a non-zero return that the dispatcher turns intoSystem.exit. These checks run via exec-maven-plugin’sjavagoal, which executes in the Maven JVM, soSystem.exitterminates Maven directly: the contributor sees the friendly diagnostic line, then the shell prompt with$? = 1, noBUILD FAILUREbanner and no[ERROR]line. The build looks like Maven crashed rather than failed a check. Replace those verify-mode tripwire exits with a thrownBuildFailure(aRuntimeExceptionwhosefillInStackTraceno-ops, since the failure surface is the printed diagnostic, not a Java stack) so exec-maven-plugin wraps it as aMojoExecutionExceptionand produces the normalBUILD FAILUREsummary. CLI/usage errors (usage(), argument errors, thecreatefile-exists path) stay onSystem.exit, since those are dev errors outside any verify phase. This adapts the change from the staleclaude/fix-maven-build-failure-yjL4Jbranch onto the current roadmap-tool structure, which has grown thecheck-adoc-tablesverify 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.
-
R404Reintroduce @sourceRow documentation when it enters the supported surface: gated on @sourceRow re-entering the supported surface; not a release priority -
R403Rethink and reintroduce @tableMethod: gated on @tableMethod re-entering the supported surface after a rethink; not a release priority