This is the rendered roadmap. Plans are authored as markdown in graphitron-rewrite/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 Rewrite Design Principles. For workflow conventions, see Workflow. 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 |
|---|---|---|---|---|
|
Rebase and squash rewrite branch onto main |
Ready |
||
|
Dimensional model pivot: slots over cross-product permits |
Spec |
2026-05-25 |
|
|
ServiceMethodCall walker carrier (R222 foundation slice) |
Ready |
2026-05-27 |
|
|
UpdateRows walker carrier (R222 UPDATE slice) with PK-or-UK identification |
Spec |
2026-05-27 |
|
|
Multi-tenant routing on top of the schema-driven ExecutionInput factory |
Spec |
2026-05-20 |
|
|
Multi-parent |
Spec |
||
|
Nested input types in @mutation fields |
Spec |
2026-05-20 |
|
|
Faceted search on |
Spec |
||
|
Type UPSERT dialect requirement on the model |
Spec |
||
|
Surface database CHECK constraints as Jakarta validation rules |
Spec |
||
|
Docs as an index into classification tests |
Ready (deferred) |
||
|
Retire |
In Progress |
||
|
Centralize ResultType column-read emission for @record parents |
Spec |
2026-05-19 |
|
|
DML payload positional input/output alignment |
Spec |
2026-05-26 |
|
|
Enumerate the capabilities graphitron delivers |
Spec |
||
|
ErrorChannel walker carrier (R222 Stage 2 slice on @service + @tableMethod) |
Spec |
2026-05-26 |
|
|
Generated GraphitronSchema emission must have bounded chain depth |
Spec |
2026-05-27 |
|
|
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 |
||
|
Replace @value with PK-default partition on UPDATE/DELETE |
Spec |
2026-05-27 |
|
|
Emit assembled schema.graphqls into generated-resources/graphitron, federation-aware |
In Progress |
2026-05-27 |
|
|
Propagate schema-applied directives (@link) from consumer SDL to generated buildSchema |
In Progress |
2026-05-27 |
|
|
Per-field direction in @order/@defaultOrder via FieldSort.direction |
In Review |
2026-05-27 |
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) -
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) -
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. -
R166GraphQLSchemaVisitor as the driver for code generation: Backlog stub. Floats an architectural idea that surfaced while diagnosing R165 (fetcher-registration-empty-body-filter). Connected to R164 (field-model-two-axis-pivot); the two reorganise different surfaces of the same pipeline and a clean field model is the natural foundation for a cleaner emission walk. (updated 2026-05-15) (blocked by dimensional-model-pivot) -
R52Lift lookup-vs-query operation taxonomy into the model: R50 named the lookup-vs-query split as a documentation-level distinction without lifting it into the model. The distinction is real and structurally consequential: lookups carry a derived VALUES table with anidxcolumn to preserve per-input-row identity, queries fold predicates into a WHERE with no input-row identity to track. Today the split is encoded only by variant identity (LookupMappingvs everything else) and routing decisions taken in individual generators. -
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. -
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 the (b-cheap) candidate keyedcolumn-field-requires-table-backed-parent. 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-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) -
R46Multi-tenant fan-out for@service: A custom resolver in a downstream Sikt project (megVedLarested) bypasses@serviceand writes the resolver by hand because the directive can’t express what it needs: for each tenant the logged-in user belongs to, open a tenant-scopedDSLContext, fan out in parallel on the executor, drop nulls, return the union. The service method itself is GraphQL-free Java; what doesn’t fit@serviceis theConnectionManagerlookup, the per-tenantDSLContextplumbing, and theexecutor.allOf().join()shape. (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 the (b-relational) candidate keyedservice-catalog-strict-tablemethod-return.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-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) -
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. -
R146Unique-index coverage as an alternative to PK for mutation cardinality safety: R144’s PK-coverage check on DELETE / UPDATE filter columns is the conservative cut: PK is the only unique key the validator considers when deciding whether to require@multiRow. Real schemas often have alternate unique constraints (e.g. a natural-key column with a unique index alongside a surrogate PK), and forcing those mutations under@multiRowdiscards the per-row cardinality guarantee the unique index would otherwise provide. This item generalises the check from "covers PK" to "covers some unique key" by exposing unique-index metadata onJooqCatalogand threading the matched-key identity through to the lookup-WHERE emitter (so RETURNING / follow-up SELECT know which columns to round-trip). Independent from R145’s UPSERT story; either can ship first. -
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,rewrite-design-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. -
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
-
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. -
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. -
R16FkJoinmodel cleanup:JoinConditionRefwrapper:FkJoin.whereFilterandConditionJoin’s condition method are typed `MethodReftoday, but they’re not arbitrary method references: the generator calls them with a fixed(srcAlias, tgtAlias)two-argument convention viaJoinPathEmitter.emitTwoArgMethodCall. The sameMethodRefinterface is also implemented byConditionFilter, which carries the separateWhereFiltercalling convention. The two shapes share a type today; conflating them has been a recurring source of confusion (the field namewhereFilteronFkJoinis itself a misnomer, since it’s a join-condition, not a filter). -
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). -
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. -
R64buildRuntimeStub accepts Rejection.Deferred directly:SplitRowsMethodEmitter.unsupportedReasonreturnsOptional<Rejection.Deferred>(per R58 Phase C, commit68a062c). Each of the fourbuildFor*callers immediately calls.message()on the result to feed theString reasonparameter ofbuildRuntimeStub, discarding the typedEmitBlockReasonthe sealedDeferredarm carries: -
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). -
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) (blocked by simplify-update-mutations-drop-value) -
R227mdBodyToAdoc: translate markdown tables to AsciiDoc when rendering roadmap:Main.mdBodyToAdoc(graphitron-rewrite/roadmap-tool/src/main/java/no/sikt/graphitron/roadmap/Main.java) is a best-effort markdown→AsciiDoc converter used byrender-adocto stage roadmap plan bodies into the documentation site. It currently passes markdown tables (| col | col |header rows separated by a|---|---|line) through verbatim, so the rendered.adoccarries raw pipe rows that Asciidoctor displays as paragraph text with literal pipes rather than a table. R223 added a verify-phase check that flags this pattern in authored.adoc, but explicitly defers the render-side hole; markdown tables embedded in.mdroadmap plans still leak into the rendered roadmap site. This item is the follow-up: extendmdBodyToAdocto detect a markdown-table block (header row + separator + body rows, terminated by a blank line) and emit an AsciiDoc|===block with a[cols=…]attribute synthesized from the separator’s column count and any GFM alignment markers. (updated 2026-05-22)
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
Other
-
R148Advance SourceLocation past description so diagnostics point at the field, not the doc block: graphql-java’sFieldDefinition.getSourceLocation()(and the same call on type, input-field, and enum-value definitions) returns the start of the description block when one is present, not the line of the field name. Build-time validator logs and the R147 LSP diagnostic surface both inherit this: an error on a documented field highlights the opening"""of the doc block rather than the field, which is misleading in the console and visually wrong in the editor squiggle. (updated 2026-05-12) -
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) -
R149End-to-end LSP publish-diagnostics test and buildOutput report-population test for R147: R147’s spec called for two tests that the implementation deferred: (updated 2026-05-12) -
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) -
R118Graphitron MCP server: knowledge-base tools for AI agents: The first non-build consumer of the knowledge base R117 frames. AI agents working in a graphitron-generated codebase ask cross-cutting questions ("what classification produced the fetcher atFilm.actors? which roadmap items mention it? which capability does it exemplify? which test exercises it?") that span the SDL, the codegen output, the runtime trace, and the roadmap. Today they grep across half a dozen file trees and reconstruct relationships from prose; the KB already holds these joins as natural-keyed tables, so the MCP server’s job is to expose the joins as tools and let the agent ask SQL or call typed lookups. This item delivers the server, not the schema (which is R117’s programme); the server queries whatever’s in the KB and grows naturally as dimensions land. (blocked by knowledge-base-programme) -
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) -
R200Honor @field(name:) in InputBeanResolver for SDL input bean/record member binding:InputBeanResolver(the resolver that populates a@serviceJava parameter from an SDLINPUT_OBJECTby matching SDL field names to JavaBean setters or canonical record components) reads zero directives. It iteratessdlOrder(iot)orrecordOrder(beanClass)and looks up the SDL name injavaMembersBySdlName, built either fromsetXsetter tails (InputBeanResolver.java:353-…) or fromRecordComponent.getName()(:338-345). When the Java member name diverges from the SDL field name, the field silently drops on a JavaBean (:303-308ultimately surfaces "no fields matching the SDL input type") or fails fatally on a record (:260-265).@field(name: "<javaMemberName>")on the SDL input field is the obvious author escape hatch and the directive’s docstring atdirectives.graphqls:29-39would naturally cover it ("names the underlying-binding target", which on anINPUT_FIELD_DEFINITIONwhose parent is a free-form@record-shaped Java bean is the Java accessor / component / setter base). The fix is symmetric to R191’s table-bound / polymorphic-hub thread: readDIR_FIELDon each SDL input field; when present, use the directive value as the lookup key intojavaMembersBySdlNameinstead of the SDL field name. R195’s jOOQ-record arm is a different shape (column → jOOQ TableField → setter walk) and is out of scope here; this item only fixes the consumer-authored bean / Java-record case where the directive should plain-old-name the Java member. The two items together restore@fieldsymmetry across input and output for free-form@record-shaped parents. (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. -
R214Infer argMapping when the @condition / @service Java signature is unambiguous:ServiceCatalog.reflectTableMethod/reflectServiceMethodbound each Java parameter to a GraphQL argument by name: a parameter matched a same-named field argument, a same-named declared context key, classified as a SOURCES shape, or was rejected with the long "either rename the Java parameter to match … or bind explicitly via the @service directive’s argMapping field" diagnostic. Authors hit this even when the pairing was structurally unique — a field with one argument and a method with one non-Table / non-Context / non-DSLContext parameter has only one possible binding, but the long-form error still required either renaming the Java parameter or writing a redundantargMapping: "javaName: gqlName". The user flagged this as a refusal point in real schemas: (updated 2026-05-21) -
R90LSP Javadoc surfacing + per-line definitions + @externalField + argMapping: R18 Phase 5 originally folded three JavaParser-driven capabilities in alongside@service/@condition/@recordautocomplete: -
R99LSP classpath scan misses sibling modules when dev goal runs from a sub-module: When a consumer runsmvn graphitron:devfrom inside one sub-module of a multi-module project (e.g.cd opptak-subgraph && mvn graphitron:dev), Maven loads only that module’s pom and the reactor contains a single project.AbstractRewriteMojoreadsMavenSession.getAllProjects()to assemble classpath roots for the LSP catalog scan (the Phase 5e widening that was meant to cover the multi-module case), so the scan only sees the current module’starget/classes. Sibling modules that hold service / condition / record classes referenced by the schema are silently absent:data.externalReferences()is empty, and the LSP returns no completions for@service(service: {className: "…"})/@condition/@record, no hover, and no unknown-class diagnostics on those references. Running from the parent withmvn -pl <module> graphitron:devworks because theregetAllProjects()returns the full reactor regardless of-plfiltering, but running from inside a module is a normal workflow (faster startup, scoped logs) and the silent failure is the worst-case shape: the user sees an empty popup and no error to grep for. -
R121LSP diagnostic for redundant @splitQuery on @record-parent fields: The build-tier warning when@splitQueryis applied to a field whose enclosing type carries@record(shipped per changelog entry 162) is only visible when the user runsmvn install(or hits a build via the IDE’s Maven integration); SDL editing is a much faster feedback loop, and the LSP can surface the same advisory inline at edit time. -
R253Make pipeline<→runtime SDL parity test pass:FederationBuildSmokeTest.emittedSdlMatchesRuntimeSchemais currently@Disabled(landed alongside R247). The test asserts, through graphql-java’sSchemaDiffing, that the on-classpathschema.graphqlsemitted bySchemaSdlEmitterdescribes the same schema a consumer rebuilding viaGraphitron.buildSchema(…)sees — the strongest invariant the spec for R247 carried for the artefact, and the one that catches both wrong-printer dispatch and runtime/codegen drift in a single assertion. (updated 2026-05-27) -
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) -
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) -
R182Retire legacy reactor and unnest graphitron-rewrite to repo root: Carved out from R26’s "Retire legacy + unnest the rewrite aggregator" sub-item so the repo-topology change has its own Spec and review trail. (updated 2026-05-19) (blocked by retire-maven-plugin) -
R95Routines as data-model citizens (jOOQ-native routine support): The rewrite has no support for resolving GraphQL fields from jOOQ-generated routines (functions and stored procedures). Legacy is shipping@experimental_procedureCall(PR #489 insikt-no/graphitron, on top of the already-mergedbaf8c10), but it cherry-picks the smallest possible slice of the design space and locks in a shape that does not generalise. The rewrite should treat routines as first-class data-model citizens alongside tables, in the same vocabulary it already uses for@table,@reference, and@field. -
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) -
R195Support jOOQ records as @service input-bean parameters via @field/@nodeId mapping: A@servicemutation whose Java parameter is a jOOQ generated*Record(rather than a consumer-authored POJO/record) cannot be populated from an SDL input type today, even when the input carries the legacy@table+ per-field@field(name: "…")+@nodeId(typeName: "…")directives that already describe the column mapping for the legacy DML path.InputBeanResolvermatches SDL field names directly against JavaBean setter names; it consults zero directives. The jOOQ record’s property names are derived from column names (UTDANNINGSSTATUSKODE→ propertyutdanningsstatuskode), so SDL fields namedstatuskode/fraDato/utdanningsspesifikasjonsIdproduce zero matches and the binding list is empty, surfacing as "bean class '…UtdanningsspesifikasjonsstatusRecord' has no fields matching the SDL input type 'EndreUtdanningsspesifikasjonsstatusInput'" atInputBeanResolver.java:307. R193 (the sealed-UnresolvedParamclassifier) is the natural home for a loud-rejection arm covering this case; this item is the feature that would make it work. (updated 2026-05-20)