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. For line/branch coverage of the source itself, see Source coverage report. Or jump to the by-theme view or the changelog. Back to home.
Active
| ID | Item | Status | Updated | Plan |
|---|---|---|---|---|
|
An agent cannot run SQL against the fact store, only the queries we anticipated |
Spec |
2026-08-21 |
|
|
Measure re-evaluation rather than naming, so a materialization cut set can be chosen on evidence |
Spec |
2026-08-27 |
|
|
The carrier refresh costs 41 seconds per capture, and it is the producer CTE inlined per driving row |
Spec |
2026-08-26 |
|
|
A top-level @service returning a @table type reads columns off the returned record instead of refetching by key |
In Review |
2026-08-26 |
|
|
@condition resolves its method by name alone, so per-participant overloads on a multitable filter are inexpressible |
Spec |
2026-08-19 |
|
|
Model the nested fetcher own outgoing per-field precise edges in CompileDependencyGraphBuilder |
Spec |
2026-07-13 |
|
|
Refuse a routine write’s first-hop condition instead of deferring it |
Spec |
2026-08-24 |
|
|
The fact schema’s prose is written for its author, not its reader |
Spec |
2026-08-26 |
|
|
The transaction demarcation difference between @mutation and @service is undocumented |
Spec |
2026-08-19 |
|
|
@defaultOrder on a @splitQuery child list is dropped at emit |
Ready |
2026-08-20 |
|
|
Agents write archeological, deliberation-narrating javadoc; the conventions and guards only cover roadmap citations |
Ready |
2026-08-26 |
|
|
The Graphitron data model |
Ready |
2026-08-11 |
|
|
Planners read facts, emitters read commands: dissolve the walk and the leaf zoo |
In Progress |
2026-08-27 |
|
|
Redirect Maven output to a log file in web sessions via .mvn/maven.config |
Spec |
2026-08-11 |
|
|
Refused patterns gather in one section instead of a residue in every table |
Spec |
2026-08-26 |
|
|
The classifier trace goes silent mid-fork: resetForTesting(null) in @AfterEach truncates leaf-coverage by test ordering |
Spec |
2026-08-19 |
|
|
Warn when a @reference path traverses a 1:N hop into a further projection |
Spec |
2026-08-19 |
|
|
A DML carrier payload with an errors field loses its return-derived write target |
Spec |
2026-08-25 |
|
|
A jOOQ record with no table cannot be a @service input parameter |
Spec |
2026-08-26 |
|
|
Decouple the roadmap tool from the generator reactor |
Spec |
2026-08-20 |
|
|
LSP-guided @reference path authoring |
Spec |
2026-08-06 |
|
|
Pull diagnostics, so a client can ask, cancel, and be told to ask again |
Spec |
2026-08-21 |
|
|
Scope and type the FK candidate hint on the record-FK and synthesis-miss surfaces |
Spec |
2026-07-22 |
|
|
Transitional surfaces say so where a reader arrives, and say why |
Spec |
2026-08-22 |
|
|
Deprecate @externalField: fold the computed-field shape into @service |
Ready |
2026-08-06 |
|
|
The stated key-column match spends its ambiguity silently; make it state its arity |
Ready |
2026-08-19 |
|
|
Upgrade graphql-java 25.0 → 26.0 |
Ready |
2026-08-08 |
|
|
Opt-in @service(transactional:) wraps the generated service invocation in a transaction |
Spec |
2026-08-19 |
|
|
The javadoc reference gate reaches test sources, and the capture-API residue it missed |
Ready |
2026-08-19 |
|
|
Declare the joined-table base-to-detail join on @discriminator(reference:) |
Spec |
2026-08-13 |
|
|
Surface database CHECK constraints as Jakarta validation rules |
Spec |
2026-08-06 |
|
|
Type-ahead search backed by native database indexes |
Ready |
2026-08-06 |
|
|
DML payload positional input/output alignment |
Spec |
2026-07-15 |
|
|
Enumerate the capabilities graphitron delivers |
Spec |
2026-07-14 |
|
|
How-to recipe and Sakila fixture for grouped collections via Field<Result<R>> @externalField + multiset |
Spec |
2026-07-14 |
|
|
IntelliJ plugin wrapping graphitron:dev LSP |
Spec |
2026-07-14 |
|
|
Name matching is a stratum: side relations, match views, and folds nowhere else |
Spec |
2026-08-18 |
|
|
Operation-driven test corpus, capability catalog, and runtime trace |
Spec |
2026-08-06 |
|
|
Resolved accessors for record-parent column reads (recordColumnReadArgs) |
Spec |
2026-07-15 |
|
|
fact-model doctrine: consumers share relations, not queries |
Ready |
2026-08-17 |
Backlog
Architecture
-
R234Support jOOQ embedded and UDT records as non-table input backings: A non-table jOOQRecordinput backing is currently admitted, not rejected:TypeBuilder.buildNonTableInputType(TypeBuilder.java:1688, dispatch at:1702-:1709) routesTableRecordsubclasses toGraphitronType.JooqTableRecordInputTypeand any otherorg.jooq.Recordsubclass to the genericGraphitronType.JooqRecordInputType; both are live sibling input arms inGraphitronType.java(:342and:357). (An earlier version of this item claimed R222 had collapsed the generic arm into a rejection; that is not the shipped state, and theBackingClassfamily that framing referenced is unbuilt. R222 remains a prose gate on classification-model direction, not a hard dependency edge, sodepends-on: []stands.) What the generic arm lacks is structure: a jOOQ embeddable record (jOOQ 3.20+ feature for grouping related columns into a typed structure) or a UDT record (PostgreSQL composite type) classifies as the same undifferentiatedJooqRecordInputType, and no real schema in graphitron-fixtures-codegen or Sakila currently exercises either as an input. Both are legitimate jOOQ-side carriers; both have stable Java accessors but noTableRefof their own. This item introduces the dedicated arm(s) those cases need, likelyEmbeddableRecord(fqClassName, embeddable: EmbeddableRef)andUDTRecord(fqClassName, udt: UDTRef)rather than leaning on the genericJooqRecordInputTypecatch-all, so each arm carries the structural metadata its downstream consumers actually want. Scope: add the new arm(s) as siblings ofJooqRecordInputType/JooqTableRecordInputType, wireTypeBuilder.buildNonTableInputTypeto 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. No consumer needs it yet; lands once a real fixture or user case surfaces. (updated 2026-07-13, created 2026-05-23) -
R848Design the materialization cut set as a whole instead of accreting it one registration at a time: Every materialization registration in the fact store was added on its own measurement, and each of those measurements was sound. Nobody has ever evaluated the resulting set as a set. The register is now twenty registrations arranged twelve layers deep, which is a shape no author chose and no document states; it is what a sequence of locally correct decisions happened to leave behind. A greedy descent finds a local optimum, and the question this item asks is whether the store is sitting in one. (updated 2026-08-26) -
R626LSP argMapping completions and diagnostics at the @routine coordinate:LspVocabulary.CanonicalOverlay.overlay()keysBehavior.ArgMappingBindingon theSchemaCoordinate.InputField("ExternalCodeReference", "argMapping")coordinate only.@routine’s `argMappingis aSchemaCoordinate.DirectiveArg, and the overlay names no@routinecoordinate at all, so@routine(argMapping:)gets no completions and no diagnostics: an author gets editor help on@serviceand silence on@routine. Add theSchemaCoordinate.DirectiveArg("routine", "argMapping")coordinate bound toBehavior.ArgMappingBinding. (updated 2026-08-13, created 2026-08-11) -
R762The census stores every class member to answer questions only ever asked by name: The classpath census stores nine relations per class: its methods, their parameters, the type references those carry, its record components and its supertypes. Two of the nine are read enumeratively, to answer a question asked before the author has typed anything. The other seven are only ever read for a class whose name is already written in the document, and they are 97% of the census. A name already written can be resolved from its classfile on demand in 0.1 ms, which is what makes the depth a choice rather than a requirement. This item stores class names and resolves members on demand. It is a second, independent lever on the same population R685 narrows by width, and the two compose. (updated 2026-08-20) -
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-08-06, created 2026-07-01) -
R719A mutation @routine carrier is decided by the payload shape, not by whether @reference was written: On aMutationroot,@routinehas two shapes. The chained form returns the table type the chain lands on. The carrier form returns a payload wrapping one data field beside an error channel, and the data field owns a post-commit re-read. Two places decide which shape a field is, and both decide it the same wrong way: by whether the author wrote@reference. (updated 2026-08-18) -
R487Batched multi-table polymorphic child fields: support parent-holds-FK correlation (explainer): The batched (list and connection) forms ofMultiTablePolymorphicEmittercorrelate each participant branch by joining the participant’s table to aparentInputVALUES table:batchedBranchCorrelationChain/parentInputSlotPredicate(renamed frombatchedBranchJoinPredicateby R458 slices 2-3) emit<participant>.<slot.targetSide()>.eq(parentInput.field("<slot.sourceSide().sqlName()>", ...)), andbuildParentInputValuesEmitteraliasesparentInput’s columns to the parent’s bound-key column names (parentSourceKey.columns(), the parent/hub primary key). This works only while every participant’s hop-0 parent side is on the bound key: a `KeyTupleWhereslot, and equally aJoinedCorrelationFK hop-0 slot, since both feed the sameparentInputSlotPredicatelookup (aJoinedCorrelationcondition hop-0 correlates on the bound key viaparentInputKeyPredicateand is unaffected). That holds for the child-holds-FK orientation. A participant whose FK lives on the parent table (customer.address_idpointing at anaddress-backed participant), whether as the single hop or heading a longer route, has its parent-side slot on that FK column instead:parentInput.field("address_id")finds no such column and returns null, so the generated code is broken at runtime. Nothing about a cross-table FK’s direction is constrained by field cardinality (theselfRefFkOnSource = !isListhint orients only same-table FKs), so a list or connection field with a parent-holds-FK participant classifies today and emits this broken form. (updated 2026-07-16) -
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, and how that composes with the tenant-keyed carrier’s lazy per-key acquisition. Session identity itself needs no per-fetcher story: the<sessionState>mount runs once at pin time as committed session state, so a late fetcher on a still-pinned connection sees it; the open question is purely whenreleaseAll(which unmounts) may run. Named as a follow-on in R429’s@defersection; this item is its tracking home now that the R429 spec is deleted. (updated 2026-08-13, created 2026-07-10) -
R694Key the @service input-bean helper dedup on binding shape, the member-axis twin of R437: Thecreate<Bean>/create<Bean>Listinstantiation helpers emitted on a<Type>Fetchersclass are deduplicated by beanClassNamealone:InputBeanInstantiationEmitter.collectTransitivelyandTypeFetcherGenerator.registerBeanHelperbothputIfAbsent(beanClass, ...), and the javadoc on the former states the assumption outright, that "two top-levelInputBean`s carrying the same bean class are assumed structurally equal (the resolver maps the same Java class to the same SDL input-object type, by construction)". Nothing enforces that. One consumer-authored bean class can back two different SDL input types at two `@servicefields on one type, and if those input types bind their fields differently (divergent@field(name:)values, or a field present in one and absent in the other) the second field’s call site routes to the first-seen helper and reads the wrongMapkeys, populating members as null with nothing in the build saying so. (updated 2026-08-17) -
R468Oracle/RAS execution-tier coverage for session identity hooks: Session identity mounts through the<sessionState>method hooks (a static<mount>method whose return value is the handle later passed to<unmount>), 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 handle, detach/destroy by handle on unmount), so the mount/unmount contract, handle capture and rebinding, and the outside-any-transaction invariant (autocommit asserted at acquisition, mount running as committed session state) 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. Originally a follow-on to the shipped connect/disconnect callable form; the method-hook rework keeps the Oracle gap open and this item remains its tracking home. (updated 2026-07-10) -
R716The MCP boundary guard names one generator package of five:StoreClientBoundaryTestasserts thatgraphitron-mcp’s main sources name no generator type, and it scans for one package prefix, `no.sikt.graphitron.rewrite.. The generator module publishes four more:no.sikt.graphitron.command,.facts,.planand.render, together about 84 classes. A fully-qualified mention of any of them in a main source passes the guard, verified by planting{@code no.sikt.graphitron.plan.SelectionPlan}in a class javadoc and watching all six cases stay green where the same probe underrewrite.fails the build. (updated 2026-08-18) -
R812The defect readers ask a window-function view for one node type’s key columns, once per refused row:NodeIdMessages.keyColumnsOfis called from inside a fetch lambda in two readers, so it issues one statement per row of the answer: (updated 2026-08-23) -
R620The dev loop reads the whole classpath twice per pass:ClasspathScanneropens every entry on the compile classpath, walks it, and parses each class file for its public methods, parameters, record components, andGraphQLScalarTypeconstants. Its sole production caller isCatalogBuilder.buildExternalReferences, and two consumers call that: the capture load, throughGraphQLRewriteGenerator.captureFacts, and the LSP catalog, throughCatalogBuilder.build. Either entry point on its own scans once.DevMojocalls both, so the dev loop scans twice: at startup (runGeneratorPassthenbuildOutputQuietly) and again on every schema save (runGeneratorPassthenbuildOutput, both inside onewithCodegenScopelambda inDevMojo.regenerate). (updated 2026-08-10) -
R835The node-id decode read costs three quarters of a second and no gate holds a figure over it: Readingintent_node_id_decodeonce, over the schemagraphitron-sakila-exampleships, takes 742 ms and visits 10603 rows to answer 47. That is the single dearest relation read measured in the fact store to date, and nothing in the tree fails when it moves:DerivedReadCostTestholds a direction rather than a number, thescanCountceilings ingraphitron-lspare held over reader surfaces rather than over relations, and no reader of the decode has a budget of its own. This item is to find the lever and, whatever the lever turns out to be, to leave a figure behind that the next change has to answer to. (updated 2026-08-25) -
R717The routine carrier’s explicit data-field path needs a correlation arm that anchors on the captured record: A@routinewrite onMutationmay return a payload carrier: a type wrapping one data field beside an errors channel. The routine call is the write, and the data field owns the post-commit re-read of the committed row. Today that re-read can only reach a table the routine’s own result columns name-match directly, because the data field is not allowed to declare@referenceat all. A payload whose target sits one or more hops past that table has no spelling, and the author’s only route is to abandon@routinefor a hand-written@service. (updated 2026-08-18) -
R841The write-payload refresh is the largest pair in the register:intent_mutation_payload_columnandintent_mutation_payload_refusalare both materialized targets, and refilling the first costs about four seconds per graph on the read-cost gate’s twelve-unit fixture: 217,475 rows visited for one evaluation ofintent_mutation_payload_column_live. Every other registration inmeta_materializequotes a refresh in the single or double digits of milliseconds. The capture that matters finishes: the sakila example build completes in under three minutes with both registrations in place. But this pair is three orders of magnitude above everything else in the register on the gate’s fixture, and nobody has timed either refresh against a real schema in isolation. (updated 2026-08-26) -
R718Three reads a chain field or a routine-bound type falls through:roadmap/routine-composition-surface-from-facts.mdderived the routine read surface from facts: a routine’s result now binds its return type, a chain’s terminus is a relation, and a hop out of a function result is keyed by a relation rather than by a loop. Three existing reads did not catch up. Each answers correctly for the population it was written for and answers wrongly, or not at all, for a field whose source is a chain or a type bound by a routine’s return. They are filed together because two of them are rules of one view and have to move as a pair, and the third is the same mistake one relation over. (updated 2026-08-18) -
R609Capture-load residuals from the fact-store delivery: The fact-store item (R595, shipped; seeroadmap/changelog.md) closed with a set of improvements its contract did not demand, recorded in its body so a later pass could take or leave them. The spec file deletes on Done, so this item preserves them. None is a defect; each is a sharpening a consumer migration or a capture pass can pick up, and they need not ship together. (updated 2026-08-10, created 2026-08-08) -
R574Clear the residual inaccuracies the @table-on-input deprecation reopen left behind: Reopening the@table-on-input deprecation window (accept, ignore, warn) left four small residuals that the acceptance criterion did not reach, surfaced by the In Review gate on that item. None is a behaviour bug and none blocked the gate, but each is a live inaccuracy or dead code, so they are worth one tidy-up pass. (updated 2026-08-03) -
R782Completions and code actions get statement-count enforcers: Six language-server surfaces read the fact store, and four of them are pinned at O(1) statements per request by a test that also asserts the count does not track the document’s size:InlayHintStatementCountTest,DeclarationHoverStatementCountTest,DeclarationDefinitionStatementCountTestandDiagnosticsStatementCountTest.CompletionsandCodeActionsread the store through the sameStoreAccess.answeringdoor and are pinned by none of them. (updated 2026-08-21) -
R647Enforce @condition table-parameter assignability against the anchor table:ServiceCatalog.reflectTableMethodclaims the reservedTable<?>slot withorg.jooq.Table.class.isAssignableFrom(p.getType()), which admits any jOOQ table and is never compared against the table the emit site will actually hand it. The emitted condition call passes a concretely typed local (InputFieldConditionFixtures.addressDistrictAlberta(table_fkt0_0, addressId)in the generatedQueryConditions), so a@conditionnaming a helper typed on the wrong generated table classifies clean and produces a javac error inside generated sources with no line back to the SDL. Real fixtures already use both widened (Condition c(Table<?> table, ...)) and concrete (Condition c(Address address, ...),Condition c(Customer table, ...)) parameter types, so both forms have to keep working. (updated 2026-08-19, created 2026-08-13) (blocked by condition-method-overload-selection) -
R747Generated input records carry no constraint annotations, so the VALIDATION handler can never fire: An author writes{handler: VALIDATION}on an@errortype and gets the whole pipeline: the generated wrapper acquires ajakarta.validation.Validator, walks every@serviceargument through it, turns eachConstraintViolationinto aGraphQLErrorthrough the generatedConstraintViolationshelper, and short-circuits into the payload’s errors slot. All of it compiles, all of it runs, and it can never produce a violation. (updated 2026-08-20) -
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, andQueryServicePolymorphicField(WithErrorChannel.javanames "root mutations, root + child services"), 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 (clustered inbuildPerCellValueList,TypeFetcherGenerator.java:2622, and its siblingbuildPerCellValueListDeduped,:2795), 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. (R413, Done, bound parent-input VALUES cells through the key column’s ConverterDataTypeat the DataLoader rows-method seam; it did not address the SDL-side default lift this item targets.) -
R727Run-record families: committed command rows and the emitted-unit census land in the store: The store answers what the schema means, and afterroadmap/planners-read-facts-emitters-read-commands.mdit will be what the plan derives commands from, but nothing records what a run concluded: which command rows the plan committed, and which Java units the render fold emitted for them. Both conclusions exist only in memory during the run, so every consumer that wants them has to reproduce the tier that produced them. The language server and the MCP cannot answer "what code did this coordinate produce" without re-deriving planner logic, and a cross-tier invariant cannot be asked at all: the enforcement gap inroadmap/list-ordering-invariant-enforcement.mdis precisely a question of the form "every coordinate the facts classify as list-shaped has a launcher row carrying an ordering", and its hardest case is a site that takes no launcher row, an absence no in-memory walk over existing rows can see but a relational anti-join between the fact stratum and a command record answers directly. (updated 2026-08-19) -
R337Surface input-field projections honestly on the lowered coordinate: Re-scoped 2026-07-25 at the R519 cutover (the tombstone’s guard condition fired: R97 is Done and the@table-on-input removal shipped the per-coordinate model without covering this surfacing). The original mechanism this file proposed, a new per-typeGraphitronTypevariant mirroring the outputNestingType, stays rejected: input classification is contextual, a function of the consuming field/coordinate, never a global property of the type (see the permanent explainerroadmap/concepts/consumer-derived-input-tables.html). (updated 2026-07-25, created 2026-06-19) -
R653The extension ordinal records JVM iteration order and calls it a position:store_graph_schema_extension.ordinal’s column comment promises "stable position in the resolved set, for faithful replay", and the value does not deliver it. `AbstractRewriteMojo.buildSchemaRecipebuilds the recipe withList.copyOf(extensions)over theSetthateffectiveSchemaFileExtensionsreturns, and the omitted-configuration path returnsRewriteContext.DEFAULT_SCHEMA_FILE_EXTENSIONS, aSet.of(...)whose iteration order is salted per JVM. So two runs of the same build write the same extension rows under different ordinals, and the comment describes a stability the column does not have. (updated 2026-08-13) -
R606The facets container derives from the store rather than minting at capture:@asFacetis the one expansion the capture walk cannot run, and the fact store currently has no other home for it, so the<Conn>Facetsand<Scalar>FacetValueshapes exist only in the assembled-schema synthesis. Capture expands a macro when its contribution is a function of one carrier’s own declaration, which is what keeps a single file the unit of an incremental refresh. The facets container fails that test: its shape reads through the carrier’s arguments into the filter input type’s fields, for the@asFacetmarker, the@field(name:)binding, and the value’s scalar and nullability. That input type is free to live in another file, so minting the container during the walk would leave it stale whenever a facet is added to a file the carrier’s own refresh never re-reads. It is an aggregate over the whole schema, not a local expansion. (updated 2026-08-07) -
R845The reference tables still state live generating patterns as hand-maintained rows:docs/architecture/reference/code-generation-triggers.adocteaches each classification through a worked example: SDL rendered from the test corpus, an outcome block stating what the pipeline made of it, and prose. Both halves are held verbatim by build guards, so an example cannot drift from what the generator does. Around those examples sit reference tables, the transitional remainder, whose rows state a verdict as hand-maintained prose with no gate at all. (updated 2026-08-26) -
R731The resolved key-column list hands out a spelling, so every consumer folds at the crossing:intent_resolved_node_key_columnanswers "which columns does this type encode a node id from" with a name: the winning tier’s own spelling, as written onSDL_PINNED, as the generated class stated it onJOOQ_METADATA, and the catalog’s own onCATALOG_PRIMARY_KEY. A name is not a resolved column, and the view’s comment says so outright: whether the name is a column the table actually has "is deliberately not asked here". Every consumer that has to match against that name therefore folds case at the crossing, because the answer it was handed is a spelling rather than a reference. (updated 2026-08-19) -
R652The stamp comment still asks the retired resolution question: Two prose surfaces still frame stamping as a filesystem resolution question, which is the question the sealed source carrier retired for schema sources.store_source.stamp’s column comment says the stamp is "NULL where nothing resolves to a regular file to hash" and then enumerates the cases, one of which is "a programmatic caller may hand a bare name"; that case is now decided by the source’s `namedarm at mint, not by a probe. Every enumerated outcome is still correct, so nothing is false, but the unifying framing describes machinery that no longer runs for this population. The predicate does survive for classpath entries, which is whyClasspathSourcessays the same thing legitimately, so the fix is to split the comment’s reasoning by population rather than to reword it wholesale. Beside it,WarmStartRefreshTest.aSchemaFileStampMatchesUntilTheFileChangescarries an assertion description reading "capture stamps a schema file that resolves to a regular file", which is the retired question applied to exactly the retired population. (updated 2026-08-13) -
R565@table-on-input rejection cascades into a misleading @mutation arg-shape error: When an input type is rejected as a type (today’s live case: the retired@table-on-input location,TypeBuilder.buildInputType), every consuming@mutationfield additionally reports@mutation fields take exactly one input-object argument; found '<arg>' of type '<Input>'. The claim is false: the argument is a single input object.FieldBuilder.resolveDmlWalkerInputArgreadslookAheadVerdict(typeName), gets the type’sUnclassifiedType, and falls into the not-an-input-object arm, so the arg-shape message stands in for "this input type did not classify". The query/filter side already has the right shape:FieldBuilder.classifyArgumentcomputesisInputLikeasInputType || (UnclassifiedType && SDL type is a GraphQLInputObjectType)and routes the second case through the plain-input path so the focused error survives. (updated 2026-08-06, created 2026-08-03) -
R509Bulk DML payloads follow input order, and warn where that cannot be guaranteed: R489 made bulk projected / discriminated mutation payloads align deterministically with the RETURNING result (therows<Name>companion’sVALUES (idx, pk)join ordered byidx). That closed the payload-vs-RETURNING half of the ordering story but left the RETURNING-vs-input half implicit, and PostgreSQL does not guarantee RETURNING order in general. The product stance this item encodes: bulk mutation payloads should match the input order wherever we can make that true by construction, and every surface where we cannot must say so loudly (a build-time warning and a manual sentence), never silently hand back database scan order. (updated 2026-07-21) -
R562Classify a synthesised connection type’s totalCount/facets fields as coordinates: A synthesised connection type’stotalCountandfacetsfields exist only as emit: the classifier mints no coordinate for them, so nothing in the fact base says a covered connection carries a count or a facet aggregate. The observable gap is theOperation.CountandOperation.Facetarms of theOPERATION_ARMSobligation (ExemptionRegistry.OPERATION_KNOWN_GAPS), which no corpus fixture can reach because no classified coordinate ever carries either arm. The exemption reasons used to cite the connection launcher’sConnectionResultcarrier fork; the corpus-command item (R543) re-anchored them on the live ground, since that fork is discharged (ResultShape.Connectioncarries the helper, carrier and facet plan today). The blocker is this model question, and both reason strings name this item as its owner. (updated 2026-08-06, created 2026-07-31) -
R471Direct-SQL OnlyChild emit for the reentry family: Owns R463’s forward obligation: consume theSource.OnlyChildarrival arm as the emit-strategy dispatch it is documented to be. R463 populated the arm (arrivalOne, direct SQL licensed) but kept emitters on leaf-identity dispatch, so anOnlyChild-classified reentry field still emits a one-element DataLoader batch. R314 re-platforms the reentry emit onto the model but lands arrival-uniform by decision (settled 2026-07-13): a direct-SQLOnlyChildemit changes query shape and SELECT counts, so it definitionally cannot ship under R314’s execution-tier-equivalence acceptance and needs its own behavioral slice with its own pins. This item is that slice; theSource.OnlyChildjavadoc pointer moves here from the R431 → R432 → R314 chain. (updated 2026-07-13) -
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) -
R273Land or retire R265’s deferred compile-tier guard: Re-scoped 2026-07-14 (file renamed fromnodeid-skip-mismatch-error-surfacing.md; the original Spec body is in git history). The item was written 2026-06-02 as a merged policy decision (skip vs throw on NodeId mismatch) plus a five-site metadata-sourcing refactor. Both halves have since been settled or claimed elsewhere: (updated 2026-08-09, created 2026-06-02) -
R531Meta-test: no registry reads in the classify-time set: The read-free classification invariant (no read of the type registry under construction during the single classify-and-emit walk) now spans several classes:FieldBuilder,ServiceCatalog,InputBeanResolver,EnumMappingResolver, andTypeBuilder’s classify path all resolve referenced-type verdicts through `TypeBuilder.lookAheadVerdict/BuildContext.lookAheadVerdictor the fixed-point indices (BuildContext.scalarVerdictsand siblings), neverctx.types. The invariant’s history is the argument for pinning it: it was believed to hold after R317/R325, held lexically forFieldBuilderonly, and was falsified by transitive helper reads while implementing the walk’s input-surface extension. Today it is enforced by one marker comment inFieldBuilderand review; the next helper that reaches forctx.types.get(...)mid-walk reopens the hole with an order-dependent misclassification no existing test is guaranteed to notice (the failure mode is often permissive: a lost rejection, not a crash). File shape: a meta-test in the same lexical-scan style asRoadmapReferenceGuardTest, scanning the classify-time set forctx.types/typeRegistry.entries()/typeRegistry.getreads, with an explicit allow-list for the deliberate post-walk readers (validator, index folds,GraphitronSchemaBuilder’s post-walk reductions and the visitor’s own sibling-independent `parentTyperead). (updated 2026-08-06, created 2026-07-24) -
R689Pre-index reads of ErrorIndex.EMPTY return a plausible wrong answer:BuildContext.errorsis initialised toErrorIndex.EMPTY, and its siblings (nodes,tables,scalarVerdicts) follow the same pattern: beforeTypeBuilder.buildClassificationIndices()runs, a reader gets a populated-looking index that answers every membership question with "no". The empty value conflates two different facts, "this schema declares no@errortypes" and "the index has not been built yet", and a pre-index reader cannot tell them apart, so it computes a plausible wrong answer instead of refusing. This has now bitten twice throughBuildContext.detectErrorsFieldShape: the routine-carrier grounding (fixed by movinggroundRoutineCarriers()after the index build) and the DML-carrier grounding (R687, which applies the same fix); in both cases the write target silently failed to ground and the payload surfaced a misleading classify-time rejection. The only enforcement today is prose comments on the call sites inTypeBuilder.prepareForWalk, which is review-only and did not propagate the last time. (updated 2026-08-17) -
R534Record key decodes erase explicit-null column writes via jOOQ from() flag reset: In a generated record-instantiation helper (JooqRecordInstantiationEmitter), key decodes load viaRecord.fromArray, whosefrom()null-skip semantics reset the touched flag of every null-valued column record-wide. Column bindings are emitted before key decodes, so a present identity field silently erases any explicit-null column write made earlier in the same helper:customerUpsert(in: {identity: {...}, details: {firstName: null}})leavesfirst_nameuntouched, while the same input without the identity group writesNULL. Whether a column write survives thus depends on an unrelated sibling field’s presence, which no author would predict. Both halves are execution-pinned (GraphQLQueryTest#customerUpsert_explicitNullNestedLeaf_collapsesToOmitted/#customerUpsert_explicitNullNestedLeaf_noIdentityDecode_writesNull) and the mechanism is documented onJooqRecordInstantiationEmitter#emitKeyDecode, discovered by R527’s truth-probe (the prior prose blamed graphql-java coercion, falsely ; coercion retains nested explicit-nulls at every depth). Candidate fixes: emit key decodes before column bindings, or load decoded keys via per-columnset()instead offromArray. Either is a behavior change to pinned execution contracts (the collapse test and the service javadoc treat the erasure as the current contract), so it needs its own design pass, not a drive-by. (updated 2026-07-25) -
R577Sealed Coordinate component on ValidationError:ValidationError.coordinateis a nullableStringwhose grain consumers re-derive by dot-splitting.ValidationError.forTypeandforFieldknow the grain at construction and collapse it to a string plusnullfor schema-wide, and thenWatchErrorFormatterreconstructs it withisTypeLevel/typeOfpredicates. A third fact about the same slot, that warnings carry no coordinate so a coordinate filter excludes them by construction, lives only as a comment inDiagnosticsTool. A sealedCoordinate { SchemaWide | TypeLevel | FieldLevel }component would make the grain a read slot instead of a parse, delete the formatter’s predicates, and turn the warnings invariant into a type fact. (updated 2026-08-06, created 2026-08-03) -
R533Single-source the null-source-guarded data-channel predicate as a classifier-assigned fact: "This fetcher short-circuits on a null source" is a fact asserted twice with nothing binding the two sites: the emitter decides it structurally (the source-shape fork inTypeFetcherGenerator.buildBatchedDataFetcheremits theenv.getSource() == nullprelude on the Record arm and theinstanceof Successnarrowing on the Outcome arm, plusFetcherEmitter.buildSingleRecordIdFromReturningFetcherValue’s guard), while `GraphitronSchemaValidator.isLocalContextGuardedDataChannelre-enumerates the safe variants as a hand-maintained allow-list whose javadoc pleads "removing the guard from an existing emitter arm must remove the variant here". Per "Decide once, at the parse boundary" (two consumers evaluating the same predicate over model facts means the branch belongs in the model), lift the guarded-data-channel verdict into a classifier-assigned fact (an accessor on the field variant or a capability), have both the emitter prelude fork and the validator read it, and delete the allow-list switch. Also decide the currently-open edge the hand-list leaves ambiguous:BatchedLookupTableFieldroutes through the same guarded builder arms but is not admitted by the validator; single-sourcing forces that verdict to be stated once, with validation coverage. Routed from the stale-comment corrections item that truth-verified the allow-list’s claims against the live emitter arms. (updated 2026-08-06, created 2026-07-25) -
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,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) -
R713The graphitron decodes read captured rows instead of the AST:GraphitronFactCaptureis 1050 lines that read the SDL AST (graphql.language.Directive,Argument,Value,AstPrinter) and write the 63-relationgraphitron_family. Beside it,SdlFactCapturereads the same AST and writes the generic directive model intographql_: definitions ingraphql_directive/_argument/_location, and applications with their argument values at all five locations (graphql_type_directive+_arg, and the schema, field, argument and enum-value pairs). (updated 2026-08-18) -
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-08-06, created 2026-06-17) -
R520@table-on-input removal housekeeping: changelog, LSP directive list, docs (Phase 4): Carved out of R97 (consumer-derived-input-tables) as the housekeeping tail of the@table-on-input removal. Follows R519 (the directive removal); nothing here is load-bearing, so it can ship any time after R519 lands. (updated 2026-07-24) -
R683Does capture still need to expand @asFacet the way it expands @asConnection?: This item exists to re-decide a question, not to carry a decision forward. It was filed once as R678, absorbed into R667 as a deliverable, and then silently dropped when R667 was repointed onto a different target. Rather than re-inherit it into R667’s successor, the question is restated here so the answer is re-derived against what is true now. (updated 2026-08-14) -
R239Lift ColumnBackedField.parentTable from emitter parameter to record component: Surfaced by R237 Phase 2 as a (b-cheap) structural-lift candidate. The classifier produces aChildField.ColumnBackedField(the R508-merged carrier) only on a table-backed parent, but the parent table itself is currently threaded intoTypeFetcherGenerator.generateTypeSpecas a parameter rather than carried on theColumnBackedFieldrecord. The switch arm readsparentTablefrom 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-07-15, created 2026-05-20) -
R123Parent-context-aware schema coordinates for per-directive Behavior policy: > Premise partly overtaken by R347 Slice 2 (noted 2026-07-15). R347 > (lsp-structural-consolidation) Slice 2 shipped a standaloneDirectivePolicy> (graphitron-lsp/.../parsing/DirectivePolicy.java):bindsLiveMethod(name)now owns the former >METHOD_VALIDATING_DIRECTIVESset, and the five copy-pasted@recordcarve-outs plus the > privately-owned method-validating set route through it. That is a third shape R123’s A/B forks > never considered, and it already resolves the set-duplication half of the smell below (step 4). It > did not, however, make the per-directive discrimination stop being a name lookup: Diagnostics still > callsDirectivePolicy.bindsLiveMethod(directiveName)at validation time. R123’s surviving value is > exactly fork A, the parent-context coordinate that carries the policy on the model so no consumer > re-derives it by directive name. Rework this item onto the shippedDirectivePolicybaseline before > implementing; do not implement the stale step 4 (the set it names is already centralized). (updated 2026-08-06) -
R506Roadmap tool as statechart driver, items as machine instances: The development workflow is a state machine interpreted by prose:roadmap/workflow.adocdescribes states, guarded transitions, entry/exit rituals (delete on Done, changelog entry, README regen), and cross-item conventions (Backlog tombstones, reopen-on-divergence), and every session re-interprets that prose by hand. The costs are structural, and they match the drift smells named indocs/architecture/principles/development-principles.adoc. The flatstatus:value splices orthogonal axes into one identifier (lifecycle position, build/evidence state, sign-off freshness; the bolted-ondeferred:boolean is the leak that proves it). Sign-offs are derived facts maintained apart from their source: nothing binds a Spec → Ready or In Review → Done approval to the content that was reviewed, so a post-approval edit silently stales the approval, mitigated only by convention. The reviewer-rule guard, the tombstone reactions, and the trunk-sync bracketing are all enforced by discipline, not by an enforcer, in a repo whose central axiom is that an invariant exists only while something fails when it breaks. (updated 2026-07-20) -
R608Sweep the DirectiveConflict.directives contract over every producer site:Rejection.InvalidSchema.DirectiveConflict.directivesnow carries a stated contract on its javadoc: every listed name is applied at the rejection’s own declaration, and a remedy the author has not written belongs in the prose. The contract was settled while typing the input-field resolution path, which found the one site that violated it (the@asConnection-on-inline-TableFieldsite listed the absentsplitQuery) and pinned the property at that site. One site is a spot check, not a contract: the other producer sites are unpinned, andmcp-aggregated-diagnosticswants to count rejections per directive, which is exactly what a counterfactual entry corrupts. (updated 2026-08-07) -
R505Tenant-index tables: per-row tenant routing off an index parent: Extracted from R45 (tenant-routing-and-execution-input, since Done and recorded inchangelog.md) on 2026-07-20 to keep its first iteration to a two-way table classification (tenant-scoped or global). This item adds the third scope: the tenant-index table, which carries the tenant column but is not partitioned; it lives on the default source and its rows point out into tenants. The canonical case is a student-to-organisation index: the query starts in the index on the default source, and each returned row names the tenant its children must be fetched from, on a different connection per row. Until this lands, that shape is served by R46’s fan-out (query every tenant and union); the index makes it targeted (one query per tenant that actually holds data). (updated 2026-07-20) -
R725The carrier recognizer publishes one scan verdict for a payload three scans judge differently:TypeBuilder.carrierBindingasks three structural scans in sequence whether an SDL payload type is carrier-shaped:BuildContext.scanStructuralDmlPayload, thenscanStructuralRoutineCarrierPayload, thenscanStructuralServiceCarrierPayload. It returns the first one that both admits and has a grounded producer binding. What it cannot express is which scans admitted, so a consumer that needs one specific family’s verdict has to go ask that scan again itself. (updated 2026-08-19) (blocked by dml-carrier-errors-field-blocks-return-derived-table) -
R66Widen string-carrier intermediates onto Rejection (R58 follow-up): R58 lifted the direct candidate-hint producers onto typedRejection.AuthorError.UnknownNamefactories. 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@conditionmethods: Let a@conditionmethod take an injectedDSLContextparameter.@conditionmethods reflect through the sharedServiceCatalog.reflectTableMethodpath (ConditionResolvercalls it for both the argument-level and field-level condition forms), whose gate currently blocks the injected context. Lifting it requiresArgCallEmitterto walkMethodRef.params()instead ofcallParams()so the injectedDSLContext(ParamSource.DslContext, emitted as thedsllocal) lands at its declaration-index slot. (updated 2026-07-15) -
R518argMapping grouping form for multi-target service fan-out (GG-376): Carved out of R97 (consumer-derived-input-tables) as its own item, the way R457/R514/R515 were split off the mutation write-target axis. R97’s body noted this phase "shares no code path, dependency, or gate with the directive-retirement axis; its only tie is the 'convention + argMapping escape valve' rationale," so it lives here now that R97 has narrowed to the consumer-derived resolution it actually shipped. Orthogonal: schedule freely, independent of R519/R520. (updated 2026-07-24) -
R71@batchKeyLifter Record return-type symmetry: (Mechanism section re-anchored 2026-07-13: the original write-up namedBatchKeyLifterDirectiveResolverand aBatchKey.LifterRowKeyedpermit, both long deleted. The substance below is unchanged and re-verified; the live surface isLifterRef/SourceKey.Reader.SourceRowsCall/Wrap.) (updated 2026-07-14) -
R517Client narrowing of the fan-out domain: a tenant-column list argument intersects the request set: R46’s fan-out domain (shipped, recorded inchangelog.md) is a request-level fact: the factory-supplied tenant set intersected with the hosted map, applied uniformly to every fanned field in the operation. It cannot express narrowing driven by the query itself, such as a client selecting two of their ten authorized institutions for one field. Today that shape is approximated with an ordinary filter argument: the per-tenant WHERE clause makes unwanted tenants return empty, but every domain tenant is still queried, so the client pays connection, session, and query cost for databases it asked nothing from. (updated 2026-07-23) -
R739Consolidate LSP byte-range result construction and rewrite/dedup nits: Residue of a discarded consolidation umbrella whose shipped slices leftgraphitron-lspwith one navigation home (Nodes/GraphqlNodeKind), one directive-policy table (DirectivePolicy), and one completion dispatcher (Completions), and whose remaining premises the fact-store migration made moot. Three small facts survived a 2026-08-19 audit of the module and are worth one polish pass: (updated 2026-08-19) -
R692Decide whether an element-less @reference on an argument or input field is an author error:@reference(path: [])is legal SDL, and on a field definition the empty list is the documented "infer the foreign key" spelling:BuildContext.parsePathresolves the single FK between the field’s start and target tables. On an argument or an input field there is no target table to infer against (both sites callparsePathwith a nulltargetSqlTableName), so the empty path stays empty, the column resolves against the field’s own table, and the carrier behaves exactly as if the author had written no directive at all. The directive is inert in those two positions. (updated 2026-08-17) -
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-07-15, created 2026-05-20) -
R240Type-token threading on MethodRef.StaticOnly + ReturnTypeRef.TableBoundReturnType: Surfaced by R237 Phase 2 as a (b-relational) structural-lift candidate.MethodRef.StaticOnlycarries a reflectedreturnTypetoken andReturnTypeRef.TableBoundReturnTypecarries the field’sTableRef, but nothing structural ties the two: where a leaf pairs them, the emitters assume the method’s return token equals the field’s table token and declare<SpecificTable> table = ...without a cast. The contract is a relationship neither carrier encodes. (updated 2026-07-25, 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) -
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) -
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-07-15, created 2026-05-21) -
R701Capture declares the columns it writes, so an insert is designed rather than every-field: An insert should name the columns the writer has data for, and that column list should be written rather than reconstructed. For 123 relations, capture writes no insert statement at all. A capture site builds a generated record, calls one typed setter per column, and hands it to the sink: (updated 2026-08-18, created 2026-08-17) -
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-07-15, created 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 consumers are build-internal (see "Consumers" below): each dimension absorbed makes the next coverage view, doc render, or static check materially cheaper to write. (updated 2026-08-06) -
R591Member payload storage home: the three leaf-homed axes: After the operation-relation programme,SqlGeneratingFieldis still the storage home for the condition, orderBy and paginate payloads (filters(),orderBy(),pagination()on the seal): on those three axes theOperationMemberrows are a view over the leaf rather than the reverse, while the write, pivot and lookup axes carry their payloads member-first. The payloads are shared by reference, so the "one payload, one member row" property holds in the object-identity sense, and the leaves dissolved regardless; the residual is directional consistency, not a correctness gap. Decide whether the three leaf-homed axes should re-home onto their member rows (making the member relation the storage home on every axis) or whether the leaf-homed shape is the right end state for payloads that are total on theTableTargetFieldseal, and record the decision where the next dissolution programme will find it. The independent gate review of the programme recorded this residual as Backlog-worthy rather than programme-reopening. (updated 2026-08-04) -
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) -
R696The authored-claim conflict view carries semantics, not a rendered message:intent_authored_claim_conflictcomputes a human-readablemessagein SQL, and it is the only relation in the schema that computes prose rather than transcribing prose something else authored. Its own column comment already concedes the character of the thing: "display only, never a dimension". The row’s job is the semantics of the violation, which it already carries asverdict,directivesand the coordinate; the sentence is a projection, and it depends on a context the store does not have. (updated 2026-08-17) -
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) -
R801View-read census and closure of the declared family bridges: The family-page item declares the base facts:meta_family_bridge, the sanctioned normalization crossings, authored in the DDL and resolve-gated only. This item derives over those declarations and closes them against what the views actually do. Layers, in order: (updated 2026-08-22, created 2026-08-21) -
R698fact-model doctrine: a view carries keys and its own products, and prose only where it was captured: The store has a modelling discipline for what a view may contain, it is followed in the DDL, and it is written down nowhere. The rule has two halves. A view merges same-grain sources, re-grains by grouping, and carries keys plus columns that are its own product; a consumer joins the view to the relations holding the payloads it wants and projects what it needs, so a view never embeds a denormalized payload to save its reader a join. And a computed column earns its place only when the calculation needs data from more than one of the joined relations; a calculation over a single base relation’s own columns belongs on that base relation instead. Because the rule is unwritten, a consumer asking for its answer in one query pulls toward flattening the answer into the model, which is the private-model smell arriving through the read path rather than through a Java taxonomy. (updated 2026-08-17)
Cleanup
-
R825A mutation test seeds film_actor rows a query test asserts the absence of:graphitron-sakila-example’s execution tier runs every test class against one shared PostgreSQL database, and two classes disagree about who owns the `film_actorseed.DmlBulkMutationsExecutionTest.deleteFilmActorsByNodeId_bulkRows_deletesAllViaRowIninserts the pairs(actor 2, film 3)and(actor 3, film 4)before its mutation and deletes them in afinally, on a comment that reasons the pairs are safe because neither is ininit.sql’s seed. That reasoning covers a sequential run and not a concurrent one. `GraphQLQueryTest.splitTableField_conditionJoin_returnsActorsPerFilmreads film 3’s actors through the condition-join split-rows path and asserts the answer is exactly{1}, so while the mutation test holds its transient row the query test’s assertion is false. Observed on a fullmvn install -Plocal-db, failing with[1, 2]against an expected[1]; the same tree passes when the class or the module runs on its own, and passed a second full build, so the two classes have to be running concurrently for it to land. The two other classes that writefilm_actor(TenantDivinedRoutingExecutionTest,TenantFanOutExecutionTest) use film ids in the hundreds and are clear of every seeded read, which is the shape the fix wants: a writer either picks rows outside every reader’s assertion window or takes a row nobody else reads. Worth answering because the failure presents as a correctness defect in condition-join emission, which is where the next reader of a red build will spend their afternoon, and because CI runs the reactor with-T 1C, so the concurrency that produces it is the normal case rather than the unlucky one. (updated 2026-08-25, created 2026-08-24) -
R740Retire the oracle-diff shadow tests, and stop the anchor gate from manufacturing them: Five tests ingraphitron/src/test/java/no/sikt/graphitron/rewrite/derive/are named*ShadowTest, and the name covers two unrelated things. Three of them make the classification walk the expected value of an assertion, which pins the walk’s bugs as invariants and makes the list of known differences the real specification. Two of them compare two spellings that both ship today, which is a consistency invariant about what a user sees and is not an oracle diff at all. Calling both "shadow" is what makes the family unreadable: a reader who opensNodeTypeShadowTestexpecting scaffolding finds a live invariant, and a reader who opensDemandShadowTestexpecting a specification finds a residue list. (updated 2026-08-19) -
R765Capture a field’s authored named type as a fact, so a wrapper-stripping expression stops being a join key: Three places in the fact model ask the same question about a field: what type does it name, once list and non-null wrappers are stripped and once a macro’s rewrite of the type expression is accounted for. All three spell it inline, identically, as (updated 2026-08-20) -
R596Cross-file xref paths naming deleted plan pages publish as 404 links: The published site carries 9 cross-filexref:links whose target page does not exist, so they render as ordinary links and 404 on click. Seven come fromroadmap/changelog.adoc(lines 653, 657, 661 twice, 739, 787, 789), which names plan pages that were deleted when their items shipped (load-bearing-annotations-r78-classifier-checks,method-name-binding-enclosing-directive-context,fkjoin-alias-dead-storage,lsp-diagnostic-redundant-splitquery-on-record,id-reference-input-field,plan-generated-fetcher-quality,plan-single-cardinality-split-query); two come fromroadmap/plans/service-short-classname-resolution.adoc:21,66namingcomputed-field-with-reference.adoc. Nothing reports them: Asciidoctor never resolves a cross-document target, it only rewrites the extension, so a missing page is as silent at build time as a missing anchor. Census taken overdocs/target/stagingby resolving eachxref:target path against the staged tree. (updated 2026-08-05) -
R604LeafCoverageReport carries a private escapeAdocCell duplicating Main’s:LeafCoverageReportdeclares a privateescapeAdocCellthat is a character-for-character duplicate ofMain’s: both escape a pipe for AsciiDoc table-cell context and nothing else. Two copies of one escaping fact means the next cell-escaping rule (or the inert-span emission R587 introduces around the same emitters) lands in one and silently not the other. Fold them into one shared helper in roadmap-tool; natural to co-locate with the R587 `InertSpansunit if that item has landed, but not dependent on it. Surfaced by the R587 Spec review, which flagged it as adjacent new scope rather than part of that item. (updated 2026-08-07) -
R790Nothing enforces that the sakila-example suite stays green when another class writes the tables it reads:graphitron-sakila-examplenow runs its test classes four at a time, and on thelocal-dbpath that CI and a contributor sandbox use, all of them share one PostgreSQL database. Eleven classes writefilmand two writecontentwhile their siblings read those tables, so a reader that asserts what a table holds rather than what its own query means fails on rows it never wrote. The property the module needs is therefore stronger than "the suite is green": it is "the suite is green while another class’s rows are in the tables it reads". Nothing enforces the second. (updated 2026-08-21) -
R578Raise the LSP unknown-input-field diagnostic to Error severity:Diagnostics.descendUnknownArgsreports an input field that the SDL vocabulary does not declare atWarningseverity. Every such field is a hard schema-load failure at build time (graphql-java rejects the directive application outright), so the editor tier is strictly softer than the build tier for a condition that cannot be ignored. An author sees a yellow squiggle, keeps typing, and the build refuses to load the schema. (updated 2026-08-03) -
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-08-06, created 2026-06-23) -
R600Retire the roadmap markdown-to-AsciiDoc translator by authoring plans in AsciiDoc: Roadmap plans are authored as markdown and published as AsciiDoc, so a mechanical translator sits between what an author writes and what ships. Its own javadoc calls itself "best-effort mechanical", which is the honest description and also the problem: it is a lossy restatement with no enforcer, and an author cannot see the difference between source and published output without a full site render. Authoring plans directly in AsciiDoc would delete it and leave one markup in the repo instead of two. (updated 2026-08-06) -
R594Snapshot axis keys are consistent across the snapshot-reporting MCP tools: Four MCP tools report the live snapshot’s availability and freshness, and they spell the two keys two different ways.McpWire.writeSnapshotAxeswritessnapshotAvailability/snapshotFreshnessand its javadoc states the reason for the prefix ("so the axes never collide with a tool’s own payload fields");diagnosticsandedgescall it.status(GraphitronMcpServer.statusResult) andschema(SchemaView) hand-roll the same exhaustive switch over theLspSchemaSnapshotpermits and write bareavailability/freshnessinstead. So the shared helper documents a convention that half its potential callers do not follow, and the divergence is duplication rather than a decision: neither hand-rolled site has a stated reason for the shorter keys. (updated 2026-08-05) -
R757A hard-killed dev session leaves a store file that never warms again: Agraphitron:devsession ended withkill -9(an OOM kill, a container stop, a hard IDE shutdown) leaves itsstore.mv.dbon disk with the schema half-written and nostore_stamprow in it.GraphitronModelStore.openAtthen finds a file at the stamped path whose stamp does not match, correctly refuses to repair or delete a file it did not write, and falls back to memory. It does so on every subsequent run, so the workspace loses warm start permanently and silently until somebody deletes the cache directory by hand. Reproduced during the store-contention diagnosis: the next opener after akill -9reportsTable "STORE_GRAPH" not found (this database is empty). (updated 2026-08-20) -
R602Decide the INPUT_OBJECT locations of @record and @table: narrow or retired-location convention: Two graphitron directives still declareINPUT_OBJECTamong their SDL locations while the semantics that once justified it are gone, and two roadmap-era doctrines give opposite answers about what to do. The retired dimensional-model umbrella’s directive-narrowing stage said the declarations shrink:@tableand@recorddropINPUT_OBJECT, followed by a fixture sweep. The shipped state says the opposite:@table(name: String) on OBJECT | INPUT_OBJECT | INTERFACE(directives.graphqls:38) deliberately keeps the location under the retired-location convention (dropping it would make graphql-java fail authored schemas with a locationless grammar error instead of a graphitron diagnostic), the classify-time rejection was downgraded to ignored-and-warned, and the model store codifies that reading:graphitron_table.type_nameis documented as "the OBJECT, INPUT_OBJECT, or INTERFACE carrying @table", with the ignored-and-warned status a detection. (updated 2026-08-06) -
R521Generated-output readability and hygiene sweep: A readability/debuggability audit of the fullgraphitron-sakila-examplegenerated tree (691 files, 54k lines, 2026-07-24) surfaced a cluster of consumer-facing defects that are individually small but collectively define the reading experience of emitted code. The calibration rule for this item: identical methods appearing in multiple files violate DRY and get deduplicated; identical code inlined within different method bodies is fine and often desirable (a specialized, breakpointable frame beats generic indirection). Typed jOOQ row arities (Row2/Row3/RowNfollow from the schema’s key shapes) stay specialized per call site; hoisting them into generic helpers is explicitly out of scope. (updated 2026-07-28, created 2026-07-24) -
R777Re-source the @error union TypeResolver ladder onto ErrorRouter.Mapping[]: "Does handler H fire on throwable T" has two spellings in the tree. TheErrorRouter.Mapping.matchfamily is the load-bearing one: all three dispatch arms (ErrorRouter.dispatch,ErrorRouter.dispatchToLocalContext, andChannelCatchArmEmitter, which inlines the loop) go through it, and the@errortype’smessage:fetcher now walks it too. The second is a hand-rolledinstanceofladder inGraphitronSchemaClassGenerator.buildErrorPolymorphicResolver, which re-implements the same three discriminators in emitter code to pick a union member by source class. (updated 2026-08-21) -
R547Retire transient roadmap-ID citations from pom.xml comments: The transient-citation rule inCLAUDE.md("Javadoc conventions") is enforced byRoadmapReferenceGuardTest, which scans Java comment and string-literal regions. Build configuration is a third habitat that no scan reaches: ninepom.xmlfiles carry roadmap-ID citations in XML comments, spanning 22 distinct IDs, and the rot has already happened. The Java-17 floor comment ingraphitron-jakarta-rest/pom.xmlexplains why the module carries no@Testclasses by citing an item whose file no longer exists underroadmap/, so a contributor reading that comment has no way to recover the reason. (updated 2026-07-26) -
R789The annotation enforcement walks skip nested classes, so an annotation on one goes unenforced: Two meta-tests ingraphitron-sakila-exampleenforce a class-level annotation rule by walkingtarget/test-classesand reflecting on what they find:no.sikt.graphitron.rewrite.test.internal.TierAnnotationEnforcementTestrequires every test class to declare a tier, andno.sikt.graphitron.rewrite.test.internal.QuarkusTestLockEnforcementTestrequires every@QuarkusTestclass to lock the shared deployment key. Both filter out any class file whose name contains$, which is how the compiler names a nested class, so a rule violation on a nested class is invisible to the walk that exists to catch it. For the lock walk that is the more expensive miss: a nested@QuarkusTestwould rejoin the failure mode the lock was introduced to close,io.quarkus.test.junit.QuarkusTestExtensioncrossing its static per-test bookkeeping slots between two classes in flight, and the resulting failure names a method on the wrong class, three files from the edit that caused it. (updated 2026-08-21) -
R635The schema drift guard covers the prefix-less relations too:SchemaIdentifierDriftCheckdecides whether a backtick-quoted identifier is the store’s to police by asking whether it starts with an observed family prefix (SchemaIdentifierDriftCheck.java:163). That predicate is exactly right for the twelve families and exactly wrong for the relations the schema deliberately places outside every family:diagnosticis cited five times across four authored architecture pages (fact-model.adoc:19,fact-model.adoc:45,pipeline-overview.adoc:38,pipeline-overview.adoc:64,typed-rejection.adoc:7), and renaming it would leave every one of those citations stale with the guard silent, which is the drift the guard exists to catch. The fix is small and needs no new source of truth: the exemption roster already enumerates precisely this set,StoreCatalogalready carries it, and the scope predicate can read the union of the family prefixes and the exemption rows' relation names. Worth checking at the same time whether the guard should scan the wider authored.adocsurface (docs/history/cites the schema too) or stay scoped todocs/architecture. (updated 2026-08-12) -
R720Three routine read-surface residues: prose naming the directive, a non-discriminating order pin, and a misfiled rejection: Three small residues from the@routineread-surface work, filed together because they are one mistake told three ways: a claim about the binding written as a claim about the directive that used to be the only way to state it. None is a defect a build catches, and none was worth holding that item’s Done gate. (updated 2026-08-18) -
R737roadmap-tool stands a fact store up outside the harness, in a module the guard does not walk:StoreFixtureGuardTeststates one rule: a test does not stand a fact store up for itself, it takes one from the harness that owns its subject. One test source in the reactor still does, and the guard cannot see it.roadmap-tool/src/test/java/no/sikt/graphitron/roadmap/SchemaReferencePagesTest.java:26callsGraphitronModelStore.open()directly, androadmap-toolis deliberately absent fromGuardScope.IN_SCOPE_MODULES, which the three guards sharing that list walk. The exclusion is not an oversight and cannot simply be reverted:roadmap-tool’s whole domain is roadmap items, so an `R<n>in its sources is a legitimate reference rather than a stale citation, andRoadmapReferenceGuardTestwould start failing on the module’s own subject matter the moment the list grew. (updated 2026-08-19) -
R528Carry backed-vs-unbacked by permit identity across GraphitronType and ReturnTypeRef: The backed-vs-unbacked axis is carried by permit identity on the result side (GraphitronType.PojoResultType.Backed, whose javadoc records that sites read the permit rather than a nullableResultType#fqClassName()), but three other carriers still smuggle it through a nullable slot, the shape the sealed hierarchy exists to prevent ("Shape the type as precisely as the fact allows"): (updated 2026-07-24) -
R702Catalog-vs-catalog name comparisons in Java fold case as a hedge; make them exact: The generator compares catalog names case-insensitively in places where both operands come from the same jOOQ catalog reading and are therefore already canonical. Case-insensitive matching is a semantic only where an author’s written name meets the catalog (an author may writefilmforFILM); between two values the catalog itself produced, the fold is a hedge inherited from the legacy generator, which folded at every comparison because it never established which strings were canonical. A hedge is not harmless: on a database using quoted identifiers, two genuinely distinct columns can differ only by case, and a folded comparison silently equates them. (updated 2026-08-18) -
R778Collapse the four verbatim copies of the CamelCase to SCREAMING_SNAKE transform: Converting a CamelCase identifier to aSCREAMING_SNAKEconstant name is spelled four times in the tree, character for character the same loop each time:BuildContext.toScreamingSnake,ErrorChannelWalker.toScreamingSnake(whose own javadoc says "Same transform asBuildContext.toScreamingSnake, kept here so the walker has no dependency on it"),FieldBuilder.toScreamingSnake, andErrorMappingsClassGenerator.byTypeConstantName. (updated 2026-08-21) -
R843Execution-tier test classes mutate shared Sakila rows other classes assert on: Thegraphitron-sakila-examplemodule runs its test classes concurrently (junit-platform.propertiessetsmode.classes.default=concurrentat a fixed parallelism of 4) against one shared PostgreSQL database. Some classes write rows into that database and clean up afterwards; others assert on the exact contents of the same tables. Nothing keeps the two apart, so a write and an assertion can overlap and the reactor build fails on a test that is correct. (updated 2026-08-26) -
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) -
R598MCP warm-degradation messages carry their tool prefix: The ambient MCP instructions promise an agent that "a paged result’s first line summarises the whole set, including the total before paging", andServerInstructionsTest.everyPagedToolLeadsWithTheUnpagedTotalpins that claim across the six tools that page throughMcpWire.page. The warm-degradation arms ofdocs.searchandcatalog.searchbreak the surrounding pattern:WarmState.degradationMessagereturns a bare notice carrying neither a tool prefix nor a count, so an agent that has learned to read the first line gets a sentence about index warming with no coordinate telling it which call produced it.catalog.search’s cold arm (GraphitronMcpServer` composes the same message directly) andDocsSearchTool.degradedare the two call sites, plusCatalogSearchIndex’s `SearchOutcome.Degraded. (updated 2026-08-06) -
R537Pin the DefaultRead-under-Outcome validate-time rejection with a named test:FetcherEmitter.resolvesViaPropertyDataFetcheranswers true for two dispositions: anErrorsFieldon thePayloadAccessortransport, and aChildField.RecordReadFieldwhoseValueLocatorisDefaultRead. The first is pinned byFetcherPipelineTestwiring assertions (its own javadoc says so); the second is not pinned anywhere. The validator consumes the predicate inGraphitronSchemaValidator.validateOutcomeChildArmSwitchto reject aWrapperArmerrors transport whose sibling data field would read a property off theOutcomesource object instead of unwrappingSuccess, andOutcomeTypeValidationTestcovers only the negative direction (outcomePayloadWithTableDataField_isNotRejected): no test asserts that theDefaultReadsibling actually rejects. Two claims rest on that unpinned chain. First,FetcherEmitter.inlineSuccessRead’s `DefaultReadarm throws and documents itself as unreachable because the validator rejects the combination first; nothing fails if the rejection stops firing, and the failure mode is a generatorIllegalStateExceptioninstead of a located build error. Second,isInlineArmSwitchedDataFieldexcludesDefaultReadfor the same reason, also unpinned. Per "Every invariant has an enforcer" indocs/architecture/principles/development-principles.adoc, a claimed pin with no named test is the review-only label the axiom warns about. Reachability is worth settling as part of the item:DefaultReadarises for@error-type parents (which carry noWrapperArmerrors field, so not that route) and for class-backed parents whose backing class did not load, so the live population is a payload result type with an unresolvable backing class. If the combination is genuinely unreachable, the honest fix is to pin that rather than the rejection, and to say so where the arm claims unreachability. (updated 2026-07-25) -
R54Rename @externalField (parallel-support, deprecation, migration):@externalFieldlifted toIMPLEMENTED_LEAVESend-to-end incomputed-field-with-reference(R48, shipped; seechangelog.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. -
R655Residue from the @service phase split: unpinned Scalar trigger, dropped Table filter, stale catalog test names: Three small things the@servicecoordinate-precedence phase split left behind. Filed at its Done gate as follow-ups because none of them breaks a contract; each is a place where the split’s new shape is not yet fully pinned or fully honest. Independent of each other, so this can ship in pieces or be split. (updated 2026-08-13) -
R536Retire the always-empty ErrorChannel.Mapped slots on the polymorphic and table-interface @service variants: Four root@servicefield variants carry anOptional<ErrorChannel.Mapped>component that is structurally always empty, and the two sides disagree about whether that dead slot makes the field a channel carrier.FieldBuilder.buildServiceFieldresolves the channel throughresolveServiceOutcomeChannel, which returnsNoChannelfor anything but aResultReturnType; the polymorphic and table-interface arms are built fromServiceDirectiveResolver.Resolved.Polymorphic(aPolymorphicReturnType) and.TableBound(aTableBoundReturnType), soMutationServicePolymorphicField,MutationServiceTableInterfaceField,QueryServicePolymorphicField, andQueryServiceTableInterfaceFieldcan never hold a present channel. The Query-side pair nonetheless implementsWithErrorChannelwhile the Mutation-side pair implements onlyServiceField, so a channel-agnostic reader consuminginstanceof WithErrorChannelsees two of the four for no stated reason. (updated 2026-07-25) -
R532Route surfaceMultiProducerRejections through bindingRejectionVerdict:TypeBuilder.bindingRejectionVerdictdocuments itself as "the single producer of the rejection-first precedence", and the walk’s equals-idempotent re-register of a seeded multi-producer demotion is claimed to hold "by construction rather than by mirrored bodies staying in sync". The consumers (lookAheadVerdictviaclassifyAndRegister, andparticipantClassification) do route through it, but the seed site itself,TypeBuilder.surfaceMultiProducerRejections, still constructs theUnclassifiedType(name, location, rejection)demotion inline: the same kind scoping (object / input object), the samelocationOfdispatch, the same rejection payload, duplicated as a mirrored body. Equality holds today and is pinned byR96RecordBindingPipelineTest.multiProducerInput_reachableThroughTheWalk_keepsTypedRejection(drift would fireTypeRegistry.register’s incompatible-classes demote arm and clobber the typed payload, failing that test), so there is no behavioral defect; but the javadoc’s "by construction" claim is only true among the consumers, and the next edit to the seed’s scoping or location computation must be caught by the test rather than being unrepresentable. The fix is small: have `surfaceMultiProducerRejectionscallbindingRejectionVerdictper declared type and register the non-null results, deleting the duplicated construction so the producer genuinely is single and the javadoc is accurate as written. (updated 2026-07-24) -
R636The generated schema reference tells the truth about view columns: Two small fidelity gaps in the generated schema reference, both about what the renderer says versus what the object is. First, H2 reports every view column as nullable, so the rendered reference tells a reader thatmeta_family.prefixis nullable one line after the prose calls it "the roster’s key, unique by gate"; the same reads wrong across everyintent_view and thediagnosticsurface. A view column’s nullability is not a fact the engine knows, so the honest render is to omit the clause for views rather than print an engine artefact as a constraint. Second,SchemaReferencePages.renderRelationgates the whole key/foreign-key/check line behind a non-empty primary key (SchemaReferencePages.java:203). Today that loses nothing: all 112 base tables declare a primary key. But nothing gates that invariant, and `FactSchemaGateTest’s partition gate reads the primary-key map, so a primary-key-less base table would pass it vacuously and then silently drop its foreign keys and check constraints from the reference. Either decouple the constraint render from the primary key, or add the primary-key totality gate the renderer is quietly assuming. (updated 2026-08-12) -
R729findColumn picks silently where two columns answer one spelled name:JooqCatalog.findColumn(Table, String)resolves a spelled column name against a table by matching the generated Java name case-insensitively, then the SQL name case-insensitively, each withfindFirstoverClass.getFields(). On a table whose column spellings collide case-insensitively, which the quoted-identifier catalogs allow, more than one column answers and the method returns whichever the reflective field order reached. That order is unspecified by the JVM, so the answer is unstable rather than merely arbitrary, which is the same pathology `intent_field_producer_method’s comment already records for overloaded method names and answers there by stating the arity instead of picking. (updated 2026-08-19) -
R573graphitron-tree-sitter-natives keeps the maven-archiver 3.6.4 hard-fail: it is deliberately not a child of the parent pom, so the pluginManagement override does not reach it: The root pom’spluginManagemententry formaven-javadoc-pluginoverrides the plugin’s bundledmaven-archiverto 3.6.5, because 3.6.4 range-checksproject.build.outputTimestampagainst a1980-01-01T00:00:02Zfloor and throws onSOURCE_DATE_EPOCH=315532800, the ZIP epoch NixOS exports fromstdenv.graphitron-tree-sitter-natives/pom.xmlis deliberately not a child ofgraphitron-rewrite-parent(its header comment gives the reason: inheriting a10-SNAPSHOTparent with no<snapshotRepository>would publish a natives jar whose parent reference no Central consumer can resolve, and would lock the natives release cadence to the next 10.x rewrite release). It therefore inlines its ownmaven-javadoc-plugin3.12.0 declaration with anattach-javadocsjarexecution, and the override does not reach it: that goal makes the sameMavenArchiver.parseBuildOutputTimestampcall and still hard-fails.failOnError=falseon that declaration does not help, because the throw happens before javadoc is invoked. (updated 2026-08-03) -
R695Lint an SDL input field that binds to no member on a JavaBean-target @service bean:InputBeanResolver.bindJavaBeanis partial by design: an SDL input field whose binding key names no setter on the bean is skipped, and the bean simply does not populate it. Only the all-fields-miss case rejects. The partiality is deliberate (a bean may legitimately ignore fields the input carries), but it means a typo in a field name, or in a@field(name:)value, produces a member that stays null with nothing in the build saying so. The record arm has no such hole: its direction-B check fails a field that names no component, on the grounds that the value would otherwise be silently dropped. (updated 2026-08-17) -
R530Manual how-to: serve your schema over HTTP (the GraphitronApplication SPI adapter): The manual documents theGraphitronApplicationSPI adapter shape nowhere: the tutorial boots the sakila example and curls/graphqlwithout ever showing the wiring, andAbstractGraphitronApplicationappears indocs/manual/only incidentally (how-to/tenant-scoping.adoc). The only consumer-facing "how do I serve my schema over HTTP" recipe isgraphitron-sakila-example/README.md’s app section, a per-module README the docs system does not treat as a primary surface; that positioning gap is why the section kept drifting (R416, R417). Write a manual how-to ("Serve your schema over HTTP") covering the SPI seam: implement `GraphitronApplication(or extendAbstractGraphitronApplication), supply the schema over the generated facade, build the engine on the owned-connection path viaGraphitron.runtime(dataSource, dialect), produce per-request execution input (with the real claims-payload derivation, not the example’s hard-coded placeholder), and depend ongraphitron-jakarta-restplus a JSON-B provider.ManualXrefIntegrityTestalready guards manual xrefs, the tutorial can point at it, and the example README’s app section can shrink to a pointer plus the adapter link. Surfaced by the R417 Spec-time principles-architect consult. (updated 2026-07-24) -
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) -
R560Un-skip graphitron-javapoet test suite:graphitron-javapoet’s surefire configuration carries `<skipTests>true</skipTests>, so the module’s 23 test files never run in any build, local or CI. The element is present in the pom as far back as visible history and carries no comment explaining it, so the reason (if there ever was one) is lost. Measured 2026-07-30 by temporarily removing that one element and runningmvn -pl graphitron-javapoet test: 400 tests run, 400 pass, 1 skipped, about 6 seconds for the whole module. This is the javapoet fork’s own regression suite for the code emitter every generated file passes through, sitting unrun for the price of one XML element. (updated 2026-07-30) -
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-sakila-example/target/generated-sources/graphitron/. -
R76Emit per-participant fieldsJoin and orderBy; replace SelectJoinStep mutation in interface fetchers: The interface emissions (the root launcher’s discriminated arm and the child twinTypeFetcherGenerator.buildTableInterfaceFieldFetcher, both composing throughrender/DiscriminatedTableFragments) emit dynamic jOOQ queries by declaring aSelectJoinStep<Record> steplocal and reassigning it insideif (alias != null)blocks (thestep = step.leftJoin(alias).on(...)fragments inDiscriminatedTableFragments). 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$project(...)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(7 274 lines) orTypeFetcherGenerator.java(6 900 lines; both counts measured 2026-07-13 and still growing) 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 (still true as of 2026-07-13), 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. -
R844Em-dash substitutions left a space-semicolon-space that reads as a typo: The house style bans em dashes in authored prose and asks for a comma, semicolon, colon, or a restructured sentence instead. In practice the em dashes were often swapped one-for-one for a semicolon, keeping the spaces around it:*Derived source table* ; built from...,Validation error ; build fails,Nothing yet ; rooted-at-parent NodeId reference. A semicolon with a space before it is not a punctuation mark English has, so it reads as a typo rather than as the pause the em dash was carrying. About 75 sites acrossdocs/architectureanddocs/manual, concentrated in table cells where the substitute is standing in for "which means" or simply for a full stop. (updated 2026-08-26) -
R10Drop the assembled-schema rebuild in favour of per-variant graphql-java forms:ConnectionPromoter.rebuildAssembledForConnections(the single producer, consuming the per-field synthesis fromConnectionPromoter.synthesiseForField; line numbers deliberately not cited, they rot) 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()). (updated 2026-08-06) -
R24Rooted-at-parent NodeId reference JOIN-projection emitter (ColumnBackedReferenceField, both arities): (Title symbol re-anchored 2026-07-13 and again 2026-07-21: the originalNodeIdReferenceFieldcarrier was renamed by R50 to the column-shapedChildField.ColumnReferenceField/CompositeColumnReferenceFieldpair, and R508 dissolved that pair into the single mergedChildField.ColumnBackedReferenceFieldcarrying arity as a column count; the file keeps its slug as its own identity. The in-code deferral no longer carries a slug pointer: it anchors to this shape through itsRejection.StubKey.VariantClasskey, a live class reference rather than a roadmap path.) -
R619Consolidate the emitted-carrier producer-binding consumers onto one seam:ProducerBindinghas three emitted-carrier arms (DmlEmitted,ServiceEmitted, and the routine arm thatroadmap/routine-mutation-payload-carrier-return.mdadds) sharing a consumer-facing shape:reflectedClass,tableRef,arrival, the correlation columns, and an identicalreflectedClass.getName().equals(tableRef.recordClass().reflectionName())compact-constructor invariant. Their genuine difference is provenance, consumed only bydescribe()and the multi-producer rejection. (updated 2026-08-10) -
R490Narrow source-direct payload errors slot to List<Object>: The source-direct payload records (e.g.FilmLookupPayload) type their errors slot asList<?>to match the dispatch lambda’sFunction<List<?>, P>parameter. A migration toList<Object>is planned as part of narrowing the source-direct dispatch contract; until it lands,List<?>keeps the lambda substitutable. The rationale survives in the record’s javadoc, but the migration itself lost its only tracking pointer when the transient roadmap citation was stripped from that comment (its former tracker has shipped), so it is filed here as its own item rather than dropped. Scope: audit source-direct payload error slots, switch toList<Object>once the dispatch contract is narrowed, and drop the interim note. Surfaced as a promotion candidate by the javadoc reference purge. (updated 2026-07-16) -
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) -
R559Fetcher-to-TenantConnections recompile edge parity: Tenant-routed fetchers reference the generatedTenantConnectionscarrier (verified in the multitenant sakila package), but the compile-dependency graph carries no fetcher-to-TenantConnections edge: not in the retired model-walking builder, and not in the plan projection (PlanCompileGraph) that replaced it, whose oracle fixtures configure no tenancy. The gap is harmless for the dev loop’s schema-edit recompiles today, becauseTenantConnections’ ABI moves only on a codegen-config change, which re-baselines through a full `compileAllrather than an incremental prune. It is still a parity hole between the emitted references and the graph: either add the edge (blanket it onto the frozen cover if its ABI is schema-invariant per configuration, or derive it from the launcher rows' tenancy facts), or record the exclusion inPlanCompileGraph’s declared-superset story, and add a tenancy-configured fixture to the three-leg oracle in `IncrementalCompileHarnessTestso the verdict is enforced rather than argued. Surfaced during the recompile-graph projection work (R549 slice 7a). (updated 2026-07-30) -
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) -
R590Verify gate for the generated migration fragment:docs/manual/_generated/supported-schema-shapes.adocis a committed materialized view over the classifier traces (LeafCoverageReport, migration mode), but unlike its siblingsupported-directives.adocit has no--verifyexecution: the CI leaf-coverage step regenerates a report artifact without comparing the committed fragment, so a regeneration from a partial trace set commits silently. This has bitten twice in one programme, in both directions (a shipped shape advertised as(not yet supported)in the user manual’s migration page, and the inverse), and each time the error was caught by hand at a review gate rather than by the build. Add a verify-mode execution for the migration fragment so a full-trace mismatch fails the build the same way the directive-support fragment already does, and decide where it runs (the trunk-gated CI step has full traces; a local-plbuild does not, so the gate needs the same no-traces short-circuit the tool already carries). (updated 2026-08-04) -
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, inrejectInputFieldDirectives,MutationInputResolver.java:498-529; the override check at:507-516readsARG_OVERRIDE, imported at:26) but the directive is a no-op at emit (no.where(...)clause is produced). Argument-level@conditionon a non-@tablemutation argument is rejected outright (foundTia.argCondition().isPresent(),:452-454). 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-07-15, created 2026-05-27)
Validation
-
R677Derive the never-unsorted-list verdict from facts, and pin the lowering the verdict cannot see: Graphitron states an invariant: a list result is never unsorted. Two checks enforce it today,GraphitronSchemaValidator.validateListRequiresOrderingand its paginated siblingvalidatePaginationRequiresOrdering, and both key on the same pair of signals: the field’s resolvedOrderBySpeclanding onNone, and the field being a member ofSqlGeneratingField. Most known violations produce neither signal, so the check passes and the rows ship in whatever order the database happened to return. (updated 2026-08-19, created 2026-08-14) -
R625Honour coercing argMapping leaf extractions on routine bindings:RoutineDirectiveResolverhardcodesnew CallSiteExtraction.Direct()on every argument-sourced binding it mints, andRoutineCallEmitter.argExpressionnever readsarg.extraction()at all. So theextractioncomponent of a routine binding’sParamSource.Argis a model fact with no consumer: an invariant with no enforcer, and the load-bearing half of the coercion residue thatroadmap/routine-chain-residue.mdrecords. (updated 2026-08-11) -
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}). -
R557Completeness sweep for @splitQuery applications: every marker consumed, inert-by-construction, or rejected:@splitQueryhas no completeness enforcer: nothing guarantees that every application of the marker either forces a batched delivery, is inert for a stated structural reason, or rejects.@tenantFanOuthas exactly this inTenantBindingIndex.sweepUnreachedFanOutMarkers, whose javadoc names the failure mode ("a marked coordinate the classification never modelled … would otherwise be silently ignored; the sweep turns it into a validate-time rejection"). The absence for@splitQueryis why the marker sat silently ignored on nesting fields until a slice of the projection-command programme stumbled on it empirically instead of a test naming it; that instance is now a classify-time deferred diagnostic at the nesting arm ofFieldBuilder, but the class stays open: the next inert position is admitted silently again. (updated 2026-07-29) -
R181Validate @order/@defaultOrder: empty directive and @index coexistence: A real user report (paraphrased) crashed the schema build: (updated 2026-07-13, 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 deliberate`Field`symbol reference without re-reading the source spec body. -
R152Pin the @nodeId(typeName:) hover’s column scoping against two tables sharing a column name: Re-scoped 2026-08-20; the bug this item was filed for is fixed and only its test pin survives. The original body is in git history. As filed on 2026-05-13, the item reported thatHovers.formatNodeType(the hover for@nodeId(typeName: "X")) typed X’s key columns through a catalog-wide linear scan that returned the first name match, so two tables holding a same-named column with diverginggraphqlTypeprojections rendered whichever the catalog enumerated first. It prescribed carrying the@tablename onCompletionData.NodeMetadataand scoping the lookup throughcatalog.getTable. (updated 2026-08-20, created 2026-05-13) -
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 theInputField.ColumnBackedReferenceFieldcarrier, 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
-
R480@oneOf definition augment defeated by descriptions quoting the definition; federated SDL fails Apollo composition: Found 2026-07-14 by composing the generated federated fixture SDL with Apollo’s composition engine (@apollo/composition2.x, the same enginerover supergraph composeruns). Composition rejects the subgraph with[sakila] Unknown directive "@oneOf", which is exactly the failure R283’s augment exists to prevent. (updated 2026-07-14) -
R627@routine binds a list-shaped argument leaf to a scalar IN parameter without rejection:WireCoercionResolver.checkScalarpasses through when the SDL leaf and the declared Java type disagree on list-ness, deliberately: its javadoc states that it leaves "that cardinality concern to the shape checks". On the@servicepath those shape checks exist downstream. A@routineIN parameter has none, so a list-shaped argument bound to a scalar parameter is accepted at classify time and emits a cast that fails at request time:@routine(argMapping: "pId: ids")againstids: [Int!]emitsenv.<Integer>getArgument("ids"), aClassCastExceptionon anArrayList. This is the cardinality sibling of the input-object footgun the argMapping seam unification closes (an input-object leaf bound to a scalar parameter, which that item rejects by name), and it reaches the same generated cast through the same uncovered gap. It is reachable today through the flat spelling and becomes easier to author once nested paths land, since a nested input field is as likely to be list-shaped as a field argument. The fix is one more condition on the routine-side leaf check that the seam item introduces: reject a list-shaped leaf bound to a non-list IN parameter, naming both shapes. Kept out of the seam item so that item does not grow a second rejection class beyond the one its consumer report names; picking it up afterwards is a small edit at a rejection site that will already exist. (updated 2026-08-11) -
R783A @defaultOrder with no arguments crashes the generator with a NullPointerException instead of rejecting: Writing@defaultOrderwith no arguments at all, rather than one ofprimaryKey:,index:orfields:, aborts the whole build with a bareNullPointerExceptionand no located message. The author gets a Maven stack trace namingjava.util.Objects.requireNonNulland has to read the generator’s source to find out which coordinate is at fault. (updated 2026-08-21) -
R822A Java-source watcher test times out under load instead of waiting for the event it needs: >CatalogRefreshTest.javaSourceWriteMovesTheStoreRowWithoutAGeneratorPassfailed one > verification build withsource refresher must fire on .java write / Expecting value to be true > but was false, and passed on an immediate standalone rerun of the same tree. The assertion is a >CountDownLatch.awaitwith a fixed millisecond budget behind a debounce; when the machine is > loaded the debounce plus the refresher walk can outrun the budget, and the test reports a > missing filesystem event rather than a slow one. (updated 2026-08-24) -
R754A String-typed field over a jOOQ-enum column reads the Java constant name, not the database literal: A GraphQL field typedStringover a column whose Postgres type is an enum returns the Java constant name, not the value the database stores. The two coincide only while every literal of that enum happens to be a valid Java identifier, so the defect is invisible in the current fixture tree and appears the moment a consumer’s enum carries a literal like'PG-13'. (updated 2026-08-20) -
R808A bridging-condition split-table execution case returns a second actor only in a full-module run:GraphQLQueryTest.splitTableField_bridgingConditionJoin_returnsActorsPerFilmingraphitron-sakila-examplefailed one fullmvnd install -Plocal-dband passed the next, on an unchanged tree. The assertion is an exact list of actor ids: it expected[1]and got[1, 2], so the read returned one row too many rather than timing out or erroring. (updated 2026-08-22) -
R798A build swap still spends its session budget loading the directive vocabulary on the watcher thread:Workspace.markAllForRecalculationcallsloadVocabularybefore it enqueues anything, and that call is a read on the session-wide reader, so it is bounded only byDevMojo.SESSION_READ_BUDGET, 30 s. The diagnostics drain has since moved off the triggering thread, which was the larger half of the same problem and is now fixed; this call did not move with it. Every build swap therefore still occupies the dev loop’s watcher thread for as long as the vocabulary read takes, and the thread it occupies is the one the loop needs for the next swap. Filed out of the drain item’s In Review review as the residue its "What changes" did not cover, rather than widened into it after approval. (updated 2026-08-21) -
R679A child lookup is rejected for a positional contract it does not have:LookupKeyDirectiveResolver.resolveAtChildrejects@asConnectionbeside@lookupKeywith "@asConnection on @lookupKey fields is invalid: @lookupKey establishes a positional correspondence between the input key list and the output list (one entry per key), which pagination would break." The rejection is right; the rationale is not. That positional correspondence is the root lookup’s contract. A child lookup coordinate narrows each parent’s list rather than filling positions, as the user manual’s@splitQuerysection and the pinned behaviour ofGraphQLQueryTest.splitLookupTableField_filterExcludesActorsNotInFilmboth say, andresolveAtChildis the only site that emits this message. So the one author who ever reads it is told their field has a property it does not have. (updated 2026-08-14) -
R846A field returning an author-declared connection type has no scope table: Backlog tombstone. The fix landed under R682, whose own scope rule claims it: a store silence a conversion’s diff turns up is that item’s deliverable rather than a separate rewrite. This file stays as the redirect and deletes when R682 reaches Done. What follows is the diagnosis as it was filed, kept because it is what the fix was measured against, plus a closing section on what actually shipped and how it differs from either option this item proposed. (updated 2026-08-27, created 2026-08-26) -
R664A growth-proof staged builder over the generated ExecutionInput factory: The generatedGraphitron.newOwnedExecutionInput(...)(and its escape-hatch sibling) takes one positional parameter per contextArgument, so its arity grows with the number of declared contextArguments plus the<sessionState>mount’s payload parameters, and every addition is a source-incompatible signature change at the consumer’s call sites. A consumer can bundle payload parameters into one carrier type today, but nothing gives the factory a growth-proof named surface. A staged builder over the factory (one named, typed step per contextArgument slot, compile-checked for completeness) would apply to contextArguments generally. Named as a separate concern in the session-identity work’s trade-offs; filed here so it survives that spec’s deletion at Done. (updated 2026-08-13) -
R847A reference path ending in a condition hop resolves no column scope: <One-paragraph problem statement: what is missing or broken, and why it matters. Replace this and add a plan body when the item moves to Spec.> (updated 2026-08-26) -
R820An authored index on the field census named-type coordinate: Several derivations joingraphql_fieldon itsnamed_typecolumn, asking which fields return a given type. No key serves that coordinate (the primary key leads with the field’s own coordinate, and the named-type reference deliberately carries no foreign key), so such a join is a seek by graph alone followed by a scan of the whole field census per driving row. The carrier read-cost item measured the fix on the read-cost gate’s twelve-unit fixture:CREATE INDEX ON graphql_field (graph_name, named_type)takesintent_carrier_routine_hopfrom 19619 scans to 2137,intent_mutation_routine_seatfrom 27531 to 10049,intent_field_error_channelfrom 1079 to 215 and the errors-field rule from 916 to 249, and it clears three of the four named-type rows that item pinned inDerivedReadCostTest.KNOWN_NON_MONOTONIC(the fourth shrinks to a 567-scan gap). The pinned rows are asserted by equality, so landing this index deletes them. (updated 2026-08-24) -
R817AuthoredClaimConflicts.fieldGrain reads a recursive view once per conflict row:AuthoredClaimConflicts.fieldGraindrives a loop over the conflict rows at field coordinates and, inside it, callsclaimsAtonce per row.claimsAtreadsintent_authored_field_claim, whose body is aWITH RECURSIVE, so one detection pass evaluates a recursive derivation as many times as there are violated field coordinates. Inside that loop it then callsenrichonce per claim, which is a second read per row of the first. (updated 2026-08-23) -
R538BgeEmbedderOnnxTest fails nondeterministically under full-reactor parallel load:BgeEmbedderOnnxTest.loadsTheRealModelAndEmbedsAtTheExpectedDimensionWithMeaningfulSimilarityfails intermittently inmvnd install -Plocal-db: 2 of 4 full-reactor runs observed failing, while 3 of 3 isolated-pl graphitron-mcpruns passed. The test is the native-binding backstop, deliberately written to run in CI’s default build, and its assertion has a generous margin (related > unrelated + 0.1) precisely so it cannot flake on numerics. Both failures blew through that margin in the wrong direction,relatedscoring belowunrelated(0.371 vs 0.458, then 0.285 vs 0.347), so the embeddings are degraded rather than merely noisy, and the two runs report different values, so the ONNX path is varying run to run. The 384-dimension assertion passes both times and the model jar’s checksum matches its recorded.sha1, so this is not a truncated or wrong model artifact; the leading hypothesis is CPU/thread contention under mvnd’s parallel module execution driving ONNX Runtime onto a degraded compute path, which would make the "cannot flake" premise in the test’s own javadoc false on a loaded machine. This matters because the test gates every-Plocal-dbbuild: a ~50% failure rate under the documented build command means every item’s Done gate intermittently reports red for a reason unrelated to that item, which is exactly what happened during the R527 review (graphitron-mcpwas untouched across that item’s whole range). Investigation should first establish whether the nondeterminism is contention-driven (pin ONNX Runtime’s intra-op thread count and re-run under load) or inherent to the quantized model’s kernels on this arch; the fix is then either a deterministic session configuration or moving the semantic assertion to a load-independent tier, not widening the margin, which would forfeit what the test exists to catch. (updated 2026-07-25) -
R632Bind the residue drainage declaration to the diagnostic view arms: The rejection residue’s drainage declaration is half-enforced.RejectionResidueDrainageTest(graphitron/src/test/java/no/sikt/graphitron/rewrite/diagnostics/RejectionResidueDrainageTest.java) carries two declarations.RESIDUE_LEAVESis genuinely pinned: it is compared against a reflective walk of the sealedRejectionhierarchy, so a new rejection cause fails the test (andRejectionFacts’ exhaustive switch) until someone edits the declaration and decides its columns, which is the property that keeps the residue from silently enlarging. `MIGRATED_FAMILY_VIEWS, the ledger of families that have gone store-native and therefore left the residue, is asserted only non-empty. Nothing forces an edit when a family actually migrates: the flip leaves the leaf set unchanged, as the test’s own javadoc concedes (drainage is per family, not per leaf, and the leaves stay because other walk sites still mint them), so the ledger is prose that happens to sit in a test file. The drainage count is what the residue’s transitional claim rests on, and a ledger nothing maintains is exactly the failure the item that built it named twice: a javadoc claiming transience over a test that cannot enforce it. (updated 2026-08-11) -
R236BuildContext nested-input candidate-hint draws from path-origin table instead of @reference terminal table:BuildContext.classifyInputFieldInternal(BuildContext.java:2437) emits a "Did you mean…" hint when a nested-input column name is unresolvable. The candidate list is built fromcatalog.columnSqlNamesOf(resolvedTable.tableName())(:2535) whereresolvedTableis the path-origin enclosing input’s@table, not the path’s terminal table. (updated 2026-07-14, created 2026-05-23) -
R523Carry field SourceLocations through the tenant-binding fold rejections: Every rejection the tenant-binding fold produces (TenantBindingIndex: thenoTenantBindingfamily, the node/entity dispatch rejections, and the whole@tenantFanOutladder) carriesSourceLocation.EMPTY, although the SDL field definition’s real location is available at each producing site. The validator therefore prints these author errors without file:line coordinates, unlike the classifier rejections produced during the walk, which makes multi-schema builds needlessly hard to debug. Thread the field definition’sSourceLocationthrough the fold’s rejection constructors; purely mechanical, no behavioural change. (updated 2026-08-06, created 2026-07-24) -
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) -
R561ConditionGluePipelineTest still scans glue bodies for the shared decode-helper call:ConditionGluePipelineTest.twoQueryFields_sharingNodeIdType_emitOneSharedHelperassertsassertThat(body).contains("decodeBarRowsOrThrow(")againstmethod.code().toString()for both condition methods, the code-string-on-generated-body patterndocs/architecture/principles/development-principles.adocbans at every tier. The lines predate the condition-command reshape (they rode the file’s rename fromQueryConditionsPipelineTest) and carry unique signal the structural assertions beside them do not:hasSize(1)on the helper set proves the registry deduplicated, but only the body scan proves both methods reference the shared helper rather than one of them inlining its own decode. The same file’smultiHopIdentityCarryingLift_emitsHelperOnLiftedTupleexplicitly declines the scan citing the ban, so the file is internally inconsistent about the rule. Resolve it structurally: either ask the cross-reference question against theCodeBlocktree (the direction R554 sets for theTypeSpecAssertionsstring-scan helpers, which this case should follow or fold into), or decide the dedup fact is fully pinned by helper-set cardinality plus the compilation tier and delete the two scans. Raised by the independent Done-gate reviewer on the condition-command item, which was explicitly not asked to fix pre-existing debt. (updated 2026-07-30) -
R654Coordinate-level verdicts masked by reflection and binding rejections outside the @service seat: The defect class R649 fixes at the@serviceseat (a reflection or parameter-binding rejection short-circuits ahead of a coordinate-level verdict living downstream in the same classifier, so a problem that belongs to the coordinate is reported as a problem with the author’s Java signature) recurs at other seats. R649’s survey of the resolver family found these instances, which that item deliberately left in place: (updated 2026-08-13) -
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) -
R741Date or refresh the parallelism figure in graphitron’s junit-platform.properties:graphitron/src/test/resources/junit-platform.propertiesjustifies enabling class-level test parallelism in this module and only this module, and cites "the 170.5s-to-117.1s measurement behind the change" as the reason the module is the first. That figure is real but was taken on a tree that no longer exists: it is the pre-batching experiment, run before the column-match sweep stopped reading its claim view once per graph. On the tree the file actually ships in, the module measures 98.4s sequential against 70.6s at four threads, so the comment overstates both the module’s sequential baseline and the win parallelism buys, by roughly a factor of two on the delta. (updated 2026-08-19) -
R544Declarative mechanism for the rejection rows the classified corpus cannot hold: The@classifiedcorpus is success-only by design: it asserts that a coordinate classifies, and to what. Every failure-path assertion therefore stayed in the enum truth table, and that is now the largest block inGraphitronSchemaBuilderTest. A re-derivation on 2026-07-25 bucketed roughly 214 of its 400ClassificationCaseconstants as rejection or warning rows:UnclassifiedField/UnclassifiedTypeoutcomes,RejectionKindand typedRejectionassertions, directive-conflict and case-clash cases, excluded-field nulls, and emit-site warning checks.roadmap/audits/classification-test-dsl-inventory.mdexcluded them from the corpus migration with an explicit promise, "a separate mechanism replaces these", and no item ever owned that mechanism. This item owns it. (updated 2026-08-06, created 2026-07-26) -
R806Derive the docs staging css/docinfo fan-out from the staged tree:docs/pom.xmlcarries a hand-maintained per-directory roster for the staged site’s assets: one<copy>ofcss/and the twodocinfo-*.htmltheme files per staged directory, every entryfailonerror="false". The roster is a copy of a fact the staged tree already states (which directories contain rendered pages), and a missed entry fails nothing: the AsciiDoctor error handler does not see it, the ant-level signal is suppressed, and the first report is a human noticing a published page with no stylesheet and no site nav. Every new docs directory (the principles section, per the principles-section-under-architecture item, is the occasion that exposed this) must remember to append two entries by hand. (updated 2026-08-22) -
R588Diagnostics forimplements Node @tableover a table with no node metadata: A type declaredimplements Node @table(name: "x")over a table whose jOOQ class publishes no__NODE_*metadata has exactly one working spelling, explicit@node, and neither of the two spellings an author would naturally reach for says so. Bareid: ID!rejects withcolumn 'id' could not be resolved in the jOOQ table; did you mean: ..., whose candidate list pushes the author toward@field(name:), which would satisfy the classifier while publishing aNodecontract that cannot round-trip throughQuery.node. Writingid: ID! @nodeIdinstead rejects with@nodeId requires the containing type to be a node type (via @node or KjerneJooqGenerator metadata), which is accurate about the rule and silent about the remedy. In both cases the author has already stated their intent unambiguously by implementing theNodeinterface, and the build declines to name the one directive that would honour it. (updated 2026-08-04) -
R779Did findings-not-fixes cut the bounce loop? Measure the plan-body blame fraction over three bounced items: R775 makes the Spec → Ready reviewer hand back findings instead of editing the plan, on the diagnosis that reviewer-authored plan prose is what feeds the bounce loop. That diagnosis is falsifiable and this item is where it gets tested, on data that does not exist until R775 has been Done for a while. R775 carries no build gate and its own Done gate turns on the prose landing, so without this item the bet is never settled. (updated 2026-08-21) -
R592Document the lint rules as a manual reference page:LintRuleis a closed set of fifteen rules with stable kebab-case ids that already cross a wire: the MCPdiagnosticstool projects each id so an agent can see which rule fired, and the consumer’s<lint>config names rules by the same ids. No page in the manual documents any of them. (updated 2026-08-04) -
R586Exemption taxonomy: unpopulated arms and the population census: The sealedExemptiontaxonomy (graphitron test sources) carries an arm no registry row constructs:HarnessSingleCataloghas zero rows inExemptionRegistry, zero readers, and no roadmap item names the per-exampleRewriteContextslot its javadoc anticipates. The file’s own precedent (the removed rides-another-rows-key arm) says an arm earns its place through population, so either the arm should be deleted or the anticipated population named by a live plan. Separately, nothing enforces this:ExemptionRegistryTest’s reflective guard finds stray `Map<..., Exemption>fields but not unpopulated taxonomy arms, so the next arm minted ahead of its population is again an audit finding rather than a build failure. Decide the arm’s fate and add the arm-population census (eachExemptionpermit constructed by at least one registered row, or carrying a stated reason for existing unpopulated). (updated 2026-08-06, created 2026-08-04) -
R593Fail CI loudly when init.sql seeding fails: The "Apply graphitron-sakila-db init.sql" step in.github/workflows/rewrite-build.ymlrunspsql -f init.sqlwith no-v ON_ERROR_STOP=1.psqlexits 0 when individual statements fail, so a broken or partially-appliedinit.sqlseeds an incomplete schema and the step reports success. The failure then surfaces one step later as the cascade.claude/web-environment.mdalready documents at length:UnclassifiedType/NoSuchElement/table … could not be resolved in the jOOQ catalogacross every pipeline test that touches the missing fixture, with nothing pointing at the seed. (updated 2026-08-04) -
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-08-06, created 2026-05-21) -
R766Five generate executions run one after another on the last node of the critical path:graphitron-sakila-exampledeclares fivegraphitron:generateexecutions. They write to disjoint output packages, share nothing but the module, and Maven runs them one after another because Maven has no notion of parallel executions within a phase. Together they are 33.1 seconds on the module that is both the last node of the build’s dependency graph and the one place three of four cores sit idle. (updated 2026-08-20) -
R334Generated argument extraction is unreadable nested-ternary one-liners: 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 an explicitly-typed named local (nevervar;GeneratedSourcesLintTest.emittedSourcesDoNotUseVarbans it in emitted code) 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-07-28, created 2026-06-18) -
R703Generated list-valued bean members emit an unchecked cast that fails a consumer build under -Werror: A@serviceinput bean whose member is a list of scalars or enums (List<String>,List<Integer>, a list of enum constants) gets a helper body containing an unchecked cast:InputBeanInstantiationEmitter.directExpr’s list arm emits `raw.get("tags") == null ? null : (List<String>) raw.get("tags"). Generated sources land in the consumer’s own build, so under-Werror(whichgraphitron-sakila-exampleitself uses) that warning is a hard compile failure, and no@SuppressWarningscan be attached to a cast sitting inside an expression. (updated 2026-08-18) -
R733Hold the build wall clock with a budget, and take the derived-read slices R732 left unmeasured: R732 harvests the three slices whose wins were measured before it was written, and stops there deliberately. This item carries the rest: the slices whose wins were still unmeasured at that point, and the guardrail without which the recovered time drifts back. It is the second pass, and it wants its own Spec cycle rather than an amendment to the first, because the guardrail is an architectural choice and the remaining slices need numbers before they can be ordered against each other. (updated 2026-08-20, created 2026-08-19) -
R288Inline TableInterfaceField children (currently N+1): > Scoped to the interface case 2026-07-15. This item originally also covered a second > per-parent sync N+1 on the directive that let a developer method supply a field’s table. That > directive was withheld from the v1 surface (R400) and has since been removed outright, taking > its leaf and fetcher with it, so nothing of that half survives. This item is the interface case > only. The file slug keeps its historical name. (updated 2026-07-15, created 2026-06-09) -
R568Javadoc reference gate skips silently on rebuilds: the plugin up-to-date check compares options and file list, not source content: Thecheck-link-referencesexecution in the root pom does not re-check a module whose javadoc already ran, because maven-javadoc-plugin’s incremental check keys on the javadoc invocation rather than on the sources. It writestarget/maven-javadoc-plugin-stale-data.txt, whose content is the option strings plus the list of source file paths and nothing else: no mtimes, no digests. On the next run it recomputes that string and, if it matches, logsSkipping javadoc generation, everything is up to date.and reports success without invoking javadoc. Editing the body of an existing file changes neither the options nor the file list, so a dangling{@link}added to a file that was already there is skipped. (updated 2026-08-03) -
R752JoinedTableReprojection dedupes same-named participant terms first-wins, silently dropping a divergent projection:JoinedTableReprojection.offolds one discriminated interface’s joined-table participants into the base slice the discriminated query projects, and deduplicates the terms first-wins across all participants:seenAliaseskeysBaseSliceTerm.InheritedRefby bare field name andBaseSliceTerm.SharedKeyby column SQL name. When two participants' same-named terms genuinely denote the same projection (the in-treeSubject/Partyfixtures, where every participant’s inherited reference resolves to the same base column), first-wins is correct. When they diverge, for example two joined-table participants declaring a same-namedColumnBackedReferenceFieldover different base columns, the second participant’s projection is dropped silently: its rows read the surviving participant’s column through the shared__rk_result-key alias, with no build-time diagnostic. This is the joined-route sibling of the single-table participant alias collision R749 fixes; it was surfaced by R749’s spec-time principles consultation and split out because it is a different producer’s defect with no in-tree oracle (no fixture has disagreeing same-named joined participants). (updated 2026-08-20) -
R707JooqRecordServiceParamPipelineTest asserts on generated helper body strings:JooqRecordServiceParamPipelineTestmatches literal fragments of generated method bodies through two private helpers,methodBody(spec, name)andsingularHelperBodies(spec), both of which render aMethodSpecto text and hand it toassertThat(...).contains(...). The helper-routing arms are where it concentrates:contendedSingularShapes_emitTwoDistinctHelpers_andEachFetcherRoutesToItsOwn,contendedListShapes_emitTwoDistinctListHelpers_andEachFetcherRoutesToItsOwnandidenticalShapesAcrossDifferentInputTypes_collapseToOneBareHelpereach assert oncreateFilmRecordN(env.getArgument(appearing (or not appearing) in a fetcher body, and one arm counts how many rendered helper bodies contain the stringRELEASE_YEAR.docs/architecture/principles/development-principles.adocbans code-string assertions on generated method bodies "at every tier", anddocs/architecture/how-to/testing.adocrepeats the ban for the pipeline tier this file sits in. (updated 2026-08-18) -
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 and clear diagnostics that resolved.CompileDiagnostic.file()now carries a canonicalfile://URI (normalised once at the javac boundary, the same spelling the schema channel publishes under), so the resolution step starts from a URI rather than a raw path to resolve under the generated-sources root. Best-effort per the R410 spec: an unresolvable entry (the(no source)sentinel, or a URI outside the generated tree) is skipped, not an error. (updated 2026-08-09, created 2026-07-03) -
R34LSP quick fixes for the @node/@nodeId migration, driven by shim facts: Pivoted 2026-07-14. This item was previously the sis-side migration tracker (phased manual schema edits driven by build-log WARN/ERROR diffing; see git history ofsis-rewrite-migration.md). The pivot replaces the manual grind with tooling: surface every site where the@nodeIdsynthesis shims fire as an LSP diagnostic carrying a ready-made fix, so sis-graph developers walk the migration diagnostic-by-diagnostic with the correct directive text offered in-editor. The shims themselves already derive everything the fix needs; today they throw that information away into a console WARN. (updated 2026-08-06) -
R616Landed-note SHA citations die when a branch is rebased before its gate: Collapsing anImplementationsection to "shipped at<sha>`" notes is a gate obligation, but the SHAs an implementer has in hand at that moment are their pre-rebase ones, and trunk-based development guarantees the rebase. The grammar item’s collapsed section cited `71f27d0,c97a8deandf4dbbe3; none resolve, because the branch was rebased when trunk moved and the commits becameb48b0f8,7eb474fanddd77f66. The citation is worthless to a later reader in exactly the case it exists for. This is generic to the workflow rather than to one item: any item that writes its own landing SHAs before its final trunk sync records numbers that will not survive. (updated 2026-08-09) -
R298Leverage Rover and GraphOS: composition checks in CI, contract verification, rover lsp in the dev loop: Pivoted 2026-07-14. This item was filed as a one-shot manual verification (build a real Apollo Federation contract once, confirm that the type-level-only@tagapplications R295 puts on synthesised Connection/Edge/PageInfo declarations satisfy contract composition; see git history offederation-tag-first-client-contract-check.md). Two things changed the shape: (updated 2026-07-15, created 2026-06-10) (blocked by oneof-augment-defeated-by-descriptions) -
R657List-cardinality accessor keys on the @service path: An accessor returningList<XRecord>fans one parent out to many batch keys. On the table-child path that is the ordinaryLOAD_MANYdispatch; on the@servicepath theMap<Key, Value>return contract assumes one key per parent, so the many case is rejected by name at classify time ("a child @service batches one key per parent") rather than silently keying on the first element. (updated 2026-08-13) -
R567Lookup coordinates: unrealized co-member payloads: A lookup-keyed coordinate can mint orderBy and paginate members at child grain that no seam realizes: the launcher lookup row’s ordering slot is deliberately empty (a lookup entails input ordering), the LookupMultiset projection arm passes no orderBy, and the@orderByrejection exists at root only, so an authored@defaultOrderor carried window on a child lookup is silently dropped at emit. Separately, the batched single-record-per-key lookup combination (a single-cardinality record-arm lookup, or a loadMany dispatch) fails loud at production (`LauncherCommands.batchedLookupRow’s guard) instead of rejecting located at validate time. (updated 2026-08-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 an ordering at all, authored or user-supplied. Neither record declares anorderBycomponent, so neither implementsSqlGeneratingField, and the emitter orders results solely by the synthetic__sort__key (the participant PK). A consumer asking for a specific order gets PK order regardless, and so does an author who declared one with@defaultOrder. (updated 2026-08-19, created 2026-06-25) -
R809LspTraceTest cases share the trace seams static state and fail each other:LspTraceTest.doubleCloseIsIgnoredfailed one fullmvnd install -Plocal-dband passed the next on a tree where no commit since the previous green build had touched anything outsideroadmap/. It expected 1 and got 2, so something was counted twice rather than not at all, and re-running the class alone puts all fourteen cases green. (updated 2026-08-22) -
R786MCP issue.report tool: dedup search over issues, roadmap and changelog, plus a draft report: A schema author who hits a graphitron bug or limitation has no tool-assisted path from "this looks wrong" to a well-formed report. Today they grep the GitHub issue tracker by hand (or don’t), and the report they eventually write rarely carries what a maintainer needs first: the graphitron version, the schema slice around the failing coordinate, and what the classifier or diagnostics actually said. The result is duplicate issues, reports of bugs already fixed in a newer version, and triage round-trips asking for the schema. (updated 2026-08-21) -
R485Model-level isFkHop/pairsOf helpers for JoinStep narrowings: R438’s self-review cleanup (1), spun out of R431 per its spec’s explicit option ("splitting it to a follow-on item is an acceptable outcome if this item runs long"; R431 slice 4 landed the coupled cleanup (2), the bridging-join consolidation intoJoinPathEmitter). The FK-hop narrowing idiom,instanceof JoinStep.Hop h && h.on() instanceof On.ColumnPairs, plus blind(On.ColumnPairs) hop.on()casts, appears inline roughly forty times across ~16 main-source files (re-count at pickup:grep -rn "instanceof JoinStep.Hop" graphitron/src/mainand"instanceof On.ColumnPairs"), each a hand-rolled repetition of one model question ("is this hop FK-derived, and what are its column pairs"). A model-levelisFkHop(JoinStep)/pairsOf(JoinStep)helper pair (natural home:JoinStepor a static onOn) replaces the inline narrowings; the exhaustive sealed-switch sites are proper dispatch and stay.TestFixtures.isFkHopalready exists as a test-side copy of the predicate and should fold onto the model helper. Acceptance mirrors R431’s: generated output byte-identical, the diff audited in isolation; full reactor green. (updated 2026-08-21, created 2026-07-15) -
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) -
R470Multilingual embedding for catalog.search (Norwegian catalog comments): Carried out of the R118 MCP-server programme (discarded 2026-07-13 with slices 1-10 shipped) as its one live unresolved open question. The MCP semantic tools (catalog.search,docs.search) embed withbge-small-en-v1.5-q(graphitron-mcp/.../rag/BgeEmbedder.java, chosen in R372 D1), which is English-only. Consumer databases, notably Sikt’s own, may carry Norwegian table and column comments; against such a catalog the semantic half ofcatalog.searchdegrades to whatever the English model happens to extract from Norwegian text, and retrieval quality on the comment signal is unquantified. The docs corpus is English, sodocs.searchis unaffected. (updated 2026-07-13) -
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) -
R631One site for the loaders graph-ownership preamble: Every post-capture writer into the fact store opens with the same graph-ownership preamble: readstore_graph.base_dirfor the graph, mint the minimal anchor row where no capture ever reached this store under the name, and refuse to touch a partition another checkout’s directory owns, warning once per writer. That rule now has two implementations.OwnedGraphPartition.prepare(graphitron/src/main/java/no/sikt/graphitron/rewrite/diagnostics/OwnedGraphPartition.java) is the shared one the two diagnostics-stratum loaders call;CompileFacts.writeRound(graphitron/src/main/java/no/sikt/graphitron/rewrite/compile/CompileFacts.java) still carries the same logic inline, because the helper was extracted for the new loaders rather than by collapsing the site it was modelled on. The two agree today and the divergence is silent when they stop: the ownership refusal is the guard that keeps one module of a shared workspace from erasing another’s rows, so a rule change applied to one copy and not the other means one family respects a boundary its neighbours ignore, with no test failing. (updated 2026-08-11) -
R662Ordering over a routine chain may name columns from any node: When a@routinechain joins on to a catalog table, every node of the chain is a live alias in the emitted query, so the database can order by columns from the routine result and the joined table alike. The generator cannot express that: ordering resolves against exactly one table, the chain’s terminus, so a column that exists only on the routine’s result is unreachable the moment a@referencehop follows it. (updated 2026-08-13) -
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) -
R496Pin MappingsConstantNameDedup.groupKey javadoc to the current three ErrorChannel arms:MappingsConstantNameDedup.groupKeyjavadoc (and its "the two namespaces never collide" analysis) describes only twoErrorChannelarms,PayloadClassandLocalContext, but the switch ingroupKey/renameChannelnow handles a third arm,ErrorChannel.Mapped, which also groups bymappingsConstantName. The collision analysis predates the third arm and understates the arm set, so a reader trusts a two-namespace guarantee the code no longer provides. Rewriting the prose to cover three arms would be fresh unpinned prose (the hazard the audit avoids); the durable fix is to pin the arm set structurally, for example an exhaustive switch or a test that fails when anErrorChannelarm carrying amappingsConstantNameis added without updating the dedup grouping. (updated 2026-07-16) -
R550Pin around the Quarkus ArC unused-bean removal flake: > Thequarkus:buildgoal ongraphitron-sakila-exampleintermittently fails CI with a >NullPointerExceptioninside Quarkus ArC’s unused-bean removal pass. It is not reproducible > locally, it is not caused by anything in our bean graph, and it is not fixed by upgrading > Quarkus. Because the goal runs in thebuildjob thatdocs-buildanddocs-deploydepend > on, a hit takes the documentation site down with it: a green fix can land on trunk and never > reachgraphitron.sikt.no. Disable the removal pass for this module so the code path cannot > run. (updated 2026-07-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-08-06, created 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) -
R628Producer-binding probe grounds a dot-path leaf parameter against the outer input type:RecordBindingResolver’s producer-binding probe resolves each reflected parameter to an SDL argument name and then grounds an input-axis observation against that argument’s type: `sdlArgNamefrom the argMapping override,inputSdl = unwrappedTypeName(arg.getType()), thenaddInputObservation(inputSdl, new ProducerBinding.RootService(paramElement, ...)). The override value it reads is head-only (the file-private parser drops everything after the first.), so for a dot-path entry the argument it grounds against is the outer input object whileparamElementis the Java type of the parameter that receives the leaf value.argMapping: "req: input.request"therefore records the outerInputSDL type as backed byreq’s Java class, which is a different type than the one that parameter actually carries. `shouldBindfilters primitives,String,Number,Boolean,Character, enums, arrays andjava.*, so the mis-grounding only fires when the leaf parameter is a consumer-authored class, which is precisely the dot-path-into-input-object shape that the argMapping seam unification widens. Fixing it means deciding what the right grounding is for a path-bound parameter (most likely the leaf field’s input type rather than the head argument’s, which needs the resolved path rather than the head string), so it is a design question about producer grounding rather than a rename. Surfaced by two independent Spec reviews of the argMapping seam unification, which deliberately left the probe’s behaviour unchanged apart from deleting its private parser. (updated 2026-08-11) -
R735Project a key column across a list of node ids into an array-valued parameter: AnargMappingpath may open a@nodeId-carryingIDwith one of its node type’s key columns. Opening a[ID!]carrying the same directive is rejected, and the rejection is a scope limit rather than a rule: a list of node ids of one type opens into the list of that key column across the decoded ids, which is a perfectly determinate value. The decode side already has the shape,decode<Record>Listmaterialising one record per element andCompositeDecodeHelperRegistryreturningList<T>on the list axis, so what is missing is the consuming end. (updated 2026-08-19) -
R788Publish stops when the verification build did not cover HEAD: R787 states a rule with no enforcer: never push a tree the verification build did not cover. Today the only thing that fails when the rule breaks is trunk CI, after the unverified tree is already on trunk, which is exactly the shape the "every invariant has an enforcer" axiom disallows. The publish skill cannot catch it either: its step 2 checks trunk divergence, and "did the build cover `HEAD`" is a different predicate, one that stays uncovered in the cases the R787 ordering makes more likely (a review fix or plan edit committed after the build ran). (updated 2026-08-21) -
R831Re-measure the performance claims written into DDL comments: The store’s relation comments carry measured performance claims, and several of them steer the next author away from a shape: this join must not be written that way, this expression must be projected into a derived table first, this collapse is a two-orders-of-magnitude regression. Each was true when it was taken. Nothing re-takes them, so a claim survives the change that retires it and goes on steering. One has now been caught:intent_argument_scope_table’s comment argued at length against joining the type binding onto a field’s stripped type expression, and the registration of `intent_resolved_type_bindingretired that hazard by making the far side of the join a table with nothing left to re-evaluate. Re-measured, the shape the comment warned against is four times faster than the shape it recommends, at either grain. The comment had been steering authors wrong for several increments, and it was found by accident rather than by a check. (updated 2026-08-25) -
R674Reconcile @service record projection: a monomorphic record return passes through while the polymorphic route auto-fetches by PK: A root@servicereturning[Miljo], whereMiljois a@table-bound type and the service selects only the primary key, resolves every non-key field tonullwith no build-time or runtime signal. Reported against 10.0.0-RC30:{ mineSynligeMiljoer { kode navn } }returns[{"kode":"demo","navn":null}, ...]althoughNAVNholds non-null values in the database. The service body is the shape the reporter expected to be enough: (updated 2026-08-14) -
R522Reconcile the emitted-code seam-pin assertion convention with the testing doc: The development principles ban code-string assertions on generated method bodies, yet the accepted emission-pin convention (R45’sTenantRoutedFetcherPipelineTest,TenantRuntimeKeyTypeTest, R46’sTenantFanOutFetcherPipelineTest) assertsTypeSpec.toString()fragments that include call-site shapes inside method bodies, on the argument that the pinned strings are the seams (carrier statics, factory keys, routing calls) rather than incidental body text. The R46 Done-gate review flagged the tension: either the testing doc gains a paragraph legitimising seam pins and drawing the line against incidental-body pins (and the existing tests get audited against that line), or the pins migrate to structuralTypeSpecassertions. Decide once, write it down, and align the three test classes; today each new reviewer re-litigates the convention. (updated 2026-07-24) -
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) -
R599Remove the stray @notGenerated and @experimental_constructType declarations:directives.graphqlsdeclares two directives that are not graphitron’s:@notGeneratedand@experimental_constructType. The declarations are a bug with real behaviour, becauseDeclaredDirectives.names()feedsSchemaDirectiveRegistry.isSurvivor: emission strips applications of names graphitron does not own, silently swallowing them from the emitted schema. For@notGeneratedthe pipeline additionally hard-rejects applications with a migration message (FieldBuilder’s output-field arm, `BuildContext’s input-field arm) and lists the name in the forbidden-carrier sets. `@experimental_constructTypehas no consumer anywhere (per the census inroadmap/audits/2026-08-06-directive-consumer-census.md); the former implementation plan for it is discarded with this item’s filing, as its premise (a graphitron feature awaiting an emitter) is void. (updated 2026-08-06) -
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) -
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) -
R665Residue pins from the R648 gate: assert the service-leaf key pins fire, and name the wrong-table typed-record parent in the no-producer diagnostic: Two small residues from the R648 In Review → Done gate, filed rather than held against the approval. First, the R648 spec’s unit-test bullet asked that one case assert the non-null pins onChildField.ServiceTableField/ChildField.ServiceRecordField(thesourceKeyandkeySourcecompact-constructorrequireNonNull`s) actually fire, not merely that construction sites satisfy them; the delivery updated every direct-construction site but shipped no fires-assertion, so removing a `requireNonNullwould today surface only as a downstream NPE inChildField.sourceShape(). Second, a diagnostic wording nit: inFieldBuilder.resolveServiceKeySource, aJooqTableRecordTypeparent holding a record of a different table than theSourceselement type names falls to theJooqRecordCarrierswitch arm and is rejected with "carries no backing class that can produce one", which is untrue for a typed record parent (it carries a backing class; it is a record of the wrong table). The advice half of the message is still correct; the description half should name the actual shape. (updated 2026-08-14) -
R498Restore or repoint the missing getting-started quieting-warnings doc referenced by the RAG dev-warm hint:RagLogQuieting.incubatorHintemits a runtime, user-facing hint string telling users to see 'getting-started, "Quieting startup warnings"', but nogetting-started.adocand no such section exist underdocs/. The R483 audit removed the stale javadoc pointer to it, but the emitted message string still points at a nonexistent doc, so a user who follows the hint lands nowhere. Because it is a user-facing message string rather than a comment, the comment-drift tools do not apply; the fix is either to author the getting-started "Quieting startup warnings" section or to repoint the hint at an existing doc, a doc/behavior change that warrants its own item. (updated 2026-07-16) -
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) -
R554Retire the generated-body string-scan helpers in TypeSpecAssertions:TypeSpecAssertionsexists to replace theassertThat(method.code().toString()).contains(...)pattern thatdocs/architecture/principles/development-principles.adocbans, but four of its helpers implement that pattern internally:hasFieldsArm,appendsRequiredColumn,armGuardsArgumentConsistency, andserviceChildKeyExtractionIsUnconditionalall scan a rendered method body for a substring. The file’s own javadoc argues this is acceptable because the fragility is confined to one place, which is a real improvement over scattering the scans across call sites, but it is a containment argument rather than a structural one: the assertions still break on emitter formatting changes that alter nothing observable, and a negative scan is worse still, sinceserviceChildKeyExtractionIsUnconditionalasserts the absence ofinstanceofand so silently flips if any unrelatedinstanceofever appears in that fetcher method. (updated 2026-07-28) -
R670Root @lookupKey plus @asConnection reports a @table error the author cannot act on: Combining@asConnectionwith@lookupKeyon a root field is rejected, which is correct, but the message names an invariant the author never violated. Measured on a root fieldfilmById(film_id: [Int!]! @lookupKey, first: Int, after: String): FilmConnection @asConnection, the field classifies toUnclassifiedFieldwith the reason@lookupKey requires a @table-annotated return type. The author did not write an untyped return; they wrote@asConnectionon a lookup, and the connection type the promoter minted is what is not@table-annotated. Following the advice as given (annotateFilmConnectionwith@table) leads nowhere. (updated 2026-08-14) -
R669RootLauncherRendererTest asserts on generated body strings:RootLauncherRendererTestasserts on the rendered text of generated method bodies. It callsrender(row).code().toString()through abody(...)helper and then matches literal Java and jOOQ source fragments against it, roughly 33contains/doesNotContaincalls across its arms (.contains("rows = filmByIdInputRows(env, filmTable)"),.contains(".values(rows).as(\"filmByIdInput\", \"idx\", \"film_id\")"), and so on).docs/architecture/principles/development-principles.adocbans exactly this: "Code-string assertions on generated method bodies are banned at every tier: they test implementation, not behaviour, and break on every refactor", with the compile and execution tiers named as the replacement.docs/architecture/how-to/testing.adocrepeats the ban for the neighbouring sub-families. The ban is review-enforced rather than build-enforced, and this file is where it has drifted furthest. (updated 2026-08-14) -
R476Route ConnectionHelper.totalCount failures through the ErrorRouter redaction contract:ConnectionHelper.totalCount(env)issues its own SQL but is wired as a bare delegate with no try/catch, so a failure (e.g. a jOOQDataAccessException, whose message embeds the rendered SQL) reaches graphql-java’s default exception handler, which copies the raw message into the client-visible errors array; this bypasses theErrorRouter.surfaceClientErrorOrRedactredaction contract every emitted fetcher honours. The R13 review (finding 5) surfaced the gap; the facets delegate now routes through the redaction path (ConnectionFetcherClassGenerator.facetsDelegate) and is the template: give totalCount the same catch arm and pin the degrade contract (nullable field resolves to null, page unaffected, redacted error) the wayGraphQLQueryTest.filmsFaceted_facetFailure_degradesToNullFacetsWithRedactedErrordoes for facets. (updated 2026-07-14) -
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) -
R660Routine write key capture fetches unordered: A list-returning@routinewrite (MutationField.MutationRoutineWriteField) produces rows in no defined order, with no directive the author can write to fix it and no diagnostic saying so. Sibling ofroadmap/routine-composition-surface-from-facts.md(R704), which fixes the same defect class on the read side; split off because the seam is different (a write’s key capture, not a read’s order surface) and the fix is not the same edit. (updated 2026-08-13) -
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 (R98-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. (updated 2026-07-13, created 2026-05-16) (blocked by multi-source-input-validation) -
R556Shared pivot/nesting projection type: fetcher read name diverges under @field(name:) remap: The fetcher side keys nested-type wiring at bare type grain:TypeFetcherGenerator.indexNestingByTypeandFetcherRegistrationsEmitter.collectNestedTypesfold pivot edges and nesting edges into one first-occurrence-wins collection per projection type name, and the emitted<Type>Fetchersclass serves every edge that reaches the type. The comment justifying the fold claims the read name derives identically on both edges. That holds only without renaming: a pivot edge’sPivotSlotField.readNameis derived from the slot’s SDL name and is read by-name (rec.get(DSL.field(DSL.name(readName)))), while a nesting edge’sColumnBackedFieldread is the typed column constant (rec.get(Tables.PARENT.COL)), whose name is the SQL column name that@field(name:)resolution produced. When a projection type is reached by both a pivot edge and a nesting edge, and a field of that type carries@field(name:)with SDL name differing from the column name, the two derivations disagree and whichever representative wins emits a read that is wrong for the other edge’s rows: a pivot-first representative reads the SDL-derived alias against a nesting parent’s row that projected the SQL column name (jOOQ throws "field is not contained in row type"), and a nesting-first representative reads the SQL column name against a pivot record whose projected alias is the SDL name (same failure, other direction). (updated 2026-07-29) -
R643Supergraph peer surface for the dev loop: A dev session already sits on every answer it refuses to give.DevMojoopens the workspace store, so the sibling subgraphs' SDL-derived facts are physically in the same file, and every read the session makes leads with its owngraph_name; a developer (or a dev agent on the MCP handle) working on one subgraph of a federated supergraph cannot ask which peer defines the entity they extend, what a peer’s@keyfields are, or which subgraphs even exist. R612 lands the two prerequisites: the declared membership fact (thestore_graph_supergraphrelation, one graph-keyed row whose presence is the declaration; standalone graphs have no row) and the read doctrine’s two-axis scope rule (enumeration overstore_graphandstore_graph_supergraphalone; payload over SDL-derived families only, scoped to same-supergraph peers; a standalone graph has no membership row to join, so its peer set is empty, and two supergraphs in one workspace store stay mutually invisible). This item builds the reader: MCP tools first (peer enumeration, peer schema questions), the LSP’s cross-subgraph navigation later or separately. Constraints inherited from R612’s doctrine, restated so they are not rediscovered: this item mints the first production spelling of the peer-set predicate and should mint exactly one (a view or one shared derivation, not a per-toolWHEREclause); enumeration is not stamp-gated, butstore_graph’s `build_file_stampfitness caveat rides beside every peer answer so staleness is rendered, not hidden; and peer answers never range over graph-private families (javac_diagnostic) or the config rows. (updated 2026-08-12) -
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-08-06, created 2026-06-19) -
R708The $project selection gate fires on names selected at any depth below its unit: Every generated$projectunit gates its contributions on a selection map: the emitted body loopsgrouped.entrySet()and dispatchesswitch (sf.getName()), one arm per contribution, so an unselected field projects nothing. The map is wrong for that job. It comes from graphql-java’sDataFetchingFieldSelectionSet.getFieldsGroupedByResultKey(), either directly (ProjectionCall.fromSelectionSet/fromEnv) or through the generatedSelectionOccurrences.mergeByResultKey, and that method groups every selected field at every depth of the sub-selection, not the unit’s immediate children. So a unit’s arm fires whenever its field name appears anywhere below it, however deep and on whatever type. (updated 2026-08-18) -
R768The build boots the fact schema 1051 times, and a reset costs a fraction of a boot: A fullmvn install -Plocal-dbexecutes the fact schema’s 1894 DDL statements 1051 times and spends 395.8 seconds inside them. The build’s sequential wall clock is 339 seconds, so the boots outweigh the build: they are about 45% of all test-class CPU in the four store-heavy modules, and roughly 80 seconds of wall clock, a quarter of the build. Nothing else measured in this reactor is close. The lever is that almost none of those boots need to be boots: emptying every clearable table costs 0.85 ms, and a full reset including re-materialization costs 9.3 ms, against a 138 ms boot. (updated 2026-08-20) -
R850The column scope named-type arm cannot be repointed while its authored-claim anti-join stands:intent_field_column_scope’s `NAMED_TYPE_TABLEarm answers where a column name written at an object-typed field resolves: the table the field’s named type binds. It navigates connections by readinggraphitron_field_synthesis, so it answers for a connection the generator synthesised and is silent about one the author declared in the SDL. That is the same silence R846 diagnosed onintent_field_scope_table, at a second site, and it means an ordering column named on a field returning an author-declared connection has no scope. (updated 2026-08-27) -
R761The dependency relation is derived after the store is stamped, so an interrupted boot leaves a warm store ordering alphabetically:GraphitronModelStore.openAtcreates a file-backed store in three steps, in this order: execute the DDL, write thestore_stamprow, derivemeta_materialize_dependencyfrom the stored view definitions. A later boot decides the file is warm by reading that stamp alone, and a warm boot deliberately skips the derivation, on the sound argument that the rows are a function of the DDL the stamp names. The order of the last two steps is what leaves a hole: a process that dies between the stamp and the derivation leaves a file every later boot accepts as warm with a dependency relation that is empty rather than derived. Nothing fails; the refresh order simply falls back to the alphabetical identity case, which is exactly the quiet wrong answer the derived order exists to prevent, and it survives every subsequent boot because the stamp says the store is complete. The window is two statements wide and the shipped DDL registers no dependent derivation, so today there are no edges to lose; the first registration whose view reads another registration’s target makes it real. Deriving before stamping closes it outright, the stamp then meaning "this file holds everything a created store holds" rather than "the DDL ran". Worth checking in the same pass whether anything else has accumulated behind the stamp, and whether the stamp write belongs at the end of creation as a matter of rule rather than as a fix to one caller. (updated 2026-08-20) -
R813The derive package names three jobs and describes none of them:no.sikt.graphitron.rewrite.deriveholds three unrelated jobs under a name that describes none of them, and the name is an attractor for exactly the mistake the rewrite is trying to avoid. Call a packagederiveand the next contributor with a derivation to place will put it there, when the standing rule is that a fact about the schema belongs in the DDL and Java holds only query and rendering. (updated 2026-08-23) -
R823The dev-executor fidelity test reads a mutating table twice and calls the difference a fidelity failure: >DevExecuteExecutionTest.query_throughTheExecutor_matchesDirectInAppExecutionproves a real > thing: the dev tool’s JSON and the app’s JSON are byte-equal, so the tool sees what the app > sees. It proves it by running{ films { filmId castMembers { … } } }twice, once through each > path, and comparing the two strings. The query is unfiltered, the two reads are sequential, and >graphitron-sakila-exampleruns its test classes concurrently against one database. A writer > class that inserts a film between the two reads makes the strings differ, and the test reports > that as a fidelity failure. (updated 2026-08-24) -
R601The diagnostic stream unifies: Graphitron’s build findings reach consumers through three parallel channels that were meant to collapse into one and never did.ValidationReportcarrieserrors: List<ValidationError>andwarnings: List<BuildWarning>as separate slots (ValidationReport.java:24-28); the LSP-alignedDiagnosticrecord shipped with the walker foundation slice but the plannedwalkerDiagnosticsslot next to them was never added (zero hits in the tree), andDiagnosticstill carries notagscomponent (Diagnostic.java:23-30). Every producer that wants to say something must pick a channel, every reader that wants the whole picture must drain three, andBuildWarningis a shape whose only reason to exist is that the unification never landed. (updated 2026-08-07, created 2026-08-06) -
R807The diagnostic view spells Java class names that nothing binds to the classes: TheRejection.*half of this item has dissolved into R803’s implementation, exactly as the sequencing note below predicted. R803’s message fork resolved onto the post-capture arm: the claim-conflict arm’s message is minted in Java intointent_authored_claim_rejection, and the variant and kind minted with it, fromRejectionFacts.classSpellingandRejectionKind.ofover the actual rejection value. Both SQL literals are gone from the DDL, the spelling is now bound to the class through the one Java site the residue writer already used, and a rename of a leaf carries into both relations by construction. The gate this item’s fallback proposed (every dottedRejection.*literal in the DDL resolves to a loadable class) has nothing left to guard, and adding it would be an enforcer for an empty population. (updated 2026-08-22) -
R774The discriminator typed-bind render-tier code-string pins duplicate what the SQL baselines already pin: The enum-discriminator fix landed the same fact at two tiers, and the cheaper one is the authoritative tier. Four assertions now read the emitted method body as a string to check that a@discriminator(value:)operand isDSL.val(..., <COL>.getDataType()):RootLauncherRendererTest.discriminatedSource_appliesDiscriminatorFilter,discriminatedSource_joinedDetailOnClause_comparesAgainstAColumnTypedBind,discriminatedSource_crossTableGate_comparesAgainstAColumnTypedBind, andTypeFetcherGeneratorTest.queryTableInterfaceField_discriminatorSubselectGate_qualifiesOffTableInstance. The same four emission sites are pinned as rendered SQL by the enum-discriminated fixture families:RootLauncherSqlBaselineTestcarries theINfilter, the joined-detail ON clause and the cross-table gate ascast(? as "public"."content_kind")/"subject_kind",DmlSqlBaselineTestcarries thevalues(...)write, andBatchedChildSqlBaselineTestcarries the batched arms. The SQL baselines are whatdevelopment-principles.adocnames as the code-string ban’s replacement ("the compile and execution tiers replace them"), so the render-tier strings are the redundant copy, and the one that breaks on any javapoet or fragment refactor that leaves behaviour intact. (updated 2026-08-21) -
R688The emitter spells a nested backing class with its binary $ name, so generated fetchers for a nested result type do not compile: A result type whose backing class is a nested Java class (a record or POJO declared inside another class, binary nameOuter$Nested) generates source that does not compile. The emit sites derive the cast target withClassName.bestGuess(fqClassName), which splits on.only, so the$survives into an import statement and a cast: (updated 2026-08-17) -
R833The execution tier fails under its own parallel run: A verification build of the whole reactor failed ingraphitron-sakila-exampleonGraphQLQueryTest.splitTableField_conditionJoin_returnsActorsPerFilm, which expected one actor per film and read two. The same test passes on its own, its whole class passes on its own, and the whole module passes on its own; the next full reactor build passed too. So the failure is not in the generated SQL and not in the test’s own assertion. It is the tier’s shared database: the module’s test classes run against one PostgreSQL instance, some of them write, and a reading test that happens to run beside a writing one sees the write. Under a sequential run the order is stable enough to hide that, and under the parallel run the reactor and CI both use it is not. (updated 2026-08-25) -
R758The fact model page never learns the materialization registry:docs/architecture/explanation/fact-model.adocis the contributor-facing home for the store’s derive-on-read doctrine, and it does not know that a registry now exists. No authored page underdocs/mentionsmeta_materialize, the_livenaming convention, or the refresh entry point, and the page’s doctrine sentence still states the narrow rule ("materialization is sanctioned above where a view cannot serve") that the registry widened: a relation may now be stored because a view expresses its rule correctly and only too slowly. (updated 2026-08-20) -
R751The meta_ family states its rows as VALUES views, which take no constraints: Themeta_family is the schema describing itself: the family roster, the placement of the relations no prefix covers, and the census that closes both against what the store actually declares. Two of its three relations are authored rosters, and both are stated asCREATE VIEW ... AS VALUES (...). (updated 2026-08-20) -
R545The model owns no emit-library vocabulary: Under the functional-core / imperative-shell cut R333 draws, facts and commands are pure data and the emit library is the shell’s business. The model does not hold to that today:ClassNameappears in 21 model files andTypeNamein 20, plusParameterizedTypeNameandArrayTypeName, and the model does not merely carry javapoet types but computes them (CallParam.deriveJavaType,RowsMethodShape.strictPerKeyType,RowsMethodShape.standardScalarJavaType,RowsMethodShape.outerRowsReturnType). One file goes further and holds rendered output:RowsMethodBody’s permits each carry an opaque `CodeBlock, and sinceSplitRowsMethodEmitterandTypeFetcherGeneratorconstruct it whileRowsMethodSkeletonconsumes it, it is a shell-to-shell handoff misfiled as a model type, with the boundary inverted (the shell owns the declaration scaffolding while a model type carries the body text).BodyParamis the counter-example proving the target shape is reachable: a sealed hierarchy of pure records with no emit vocabulary at all. Measurements and their method are inroadmap/audits/2026-07-26-fcis-command-layer-distance.md. (updated 2026-07-26) -
R756The orphan sweep never visits four subpackages the generator emits into: Clean removal is one of the three clauses of the generator’s output contract: a compilation unit the schema no longer calls for is swept out of the output rather than left behind as an orphan that still compiles and still resolves.GraphQLRewriteGenerator.sweepOrphansimplements it by walking a fixed list of subpackages it considers its own, deleting every.javafile in them that this run did not emit. (updated 2026-08-20) -
R829The payload-returning UPDATE arms cross-partition agreement check has no compile or execution coverage:TypeFetcherGeneratorhas four consumers of theUpdateRowscarrier: direct-return and payload-returning, each single-row and bulk. All four now emit the cross-partition value-agreement preamble that compares two input fields' decoded values for one SQL column before any DML runs. Two of them,buildCarrierUpdateChainSingleandbuildCarrierBulkPerRowUpdateBody, gained that preamble only recently, closing a gap where a self-FK overlap reaching a payload-returning UPDATE went unchecked entirely. (updated 2026-08-25) -
R821The payload-returning UPDATE arms emit no within-SET value-agreement check: Two input fields can write the same SET column when at least one of them carries a@nodeIddecode. The walker admits that overlap deliberately (an all-plain overlap is thePlainColumnCollisionreject; a decode-involving one is meant to be reconciled at runtime), and the direct-return single-row UPDATE emitsemitSetAgreementPreambleto compare the two decoded values before the DML. The two payload-returning UPDATE arms,TypeFetcherGenerator.buildCarrierUpdateChainSingleandbuildCarrierBulkPerRowUpdateBody, do not. They build the SET map withemitSetMapPutsand no preamble, so the secondMap.putsilently clobbers the first and the caller’s other value is discarded with no error. (updated 2026-08-24) -
R827The registered targets index exemptions are a Java set in a test, so the measurement that argues each one is not in the model:MaterializeRegistryGateTestgates that every registered materialization target carries a declared index, and exempts four of them throughNO_INDEX, aSet.ofof four relation names in the test class. Beside it sits about forty lines of javadoc carrying the measurement that argues each exemption:intent_resolved_type_bindingtakesintent_argument_scope_table’s source view from 559 scans to 952 on the coordinate its thirteen readers join; `intent_errors_fieldtakesintent_carrier_routine_hopfrom 3876 to 8136 andintent_mutation_routine_seatfrom 28857 to 33117, with no reader improving;intent_carrier_data_fieldmoves no reader at all, to the scan. Each figure was taken over every view whose derivation reaches the target, with statistics current on both sides. (updated 2026-08-24) -
R802The scan-count ceiling javadoc contradicts itself about what a ceiling catches: `SurfaceScanCountTest’s class javadoc states two things about its own ceilings that cannot both be true. One paragraph says "Every ceiling here was set from both shapes measured on this fixture, and each was confirmed to fail with the defect reinstated", and the next says "The ceilings alone would not have caught the defect that prompted them" and that the excess is one "no ceiling anybody would defend". A contributor adding a ceiling reads both and cannot tell how strong the instrument is meant to be. (updated 2026-08-22) -
R257UpdateRowsWalker raw-SDL substrate absorption: R246 shippedUpdateRowsWalkeras a translator over the already-classifiedInputFieldpermits (the admitted column carriers, since R508 the two merged leavesColumnBackedField/ColumnBackedReferenceFieldeach carrying arity 1..N, 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) -
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) -
R558Validator mirrors for launcher-surfaced classification gaps: The root launcher migration (seeroadmap/changelog.mdwhen its item lands) surfaced two classifier-side invariants with no validate-time twin, both recorded on that item’s slice logs and both cheap to close now that the launcher relation single-sources the relevant populations. (1) is closed: the deterministic-order rule used to skip the root@routinechain, first by capability non-membership and later by an explicit carve-out on the source axis, androadmap/routine-composition-surface-from-facts.mdremoved the carve-out rather than re-sourcing the rule off the launcher relation. A routine-backed list now fails the build unless it names an order, with a message forked on terminus kind. (2) AGraphitronType.TableInterfaceTypeparticipant without@discriminatorclassifies and renders the legacy silent shape (its projection contributes but its rows are unroutable and its gated JOIN arms are skipped), guarded only by one documented renderer gate inrender/DiscriminatedTableFragments; the honest fix is a parse-time rejection (or validator drain) so the shape fails the build instead of returning rows the TypeResolver cannot route. Related enforcement seams to keep aligned:GraphitronSchemaValidator.validateJoinedTableReprojection(the joined-table reprojection fold’s deferral drain) and theTypeBuilder.buildParticipantListrejection for classified non-table members of a discriminated interface, which has no reaching SDL fixture yet (an@errorimplementor is the nearest shape) and deserves one when this item lands. (updated 2026-07-30, created 2026-07-29) -
R767graphitron-maven-plugin writes its descriptor twice and runs its three ITs one at a time:graphitron-maven-plugintakes 32.8 seconds, runs alone throughout, and uses about one core. Two of its goals account for 18.6 s of that and neither needs to cost what it costs:plugin:descriptorruns twice for the same output, andmaven-invoker-pluginruns three independent integration projects sequentially. (updated 2026-08-20) -
R764graphitron-model ships its junit-platform.properties to four consumers that never asked for it:graphitron-model/src/test/resources/junit-platform.propertiesturns on four-thread class-level test parallelism for that module. It also rides along in the module’s test-jar, and four modules consume that test-jar at test scope:graphitron,graphitron-lsp,graphitron-mcpandgraphitron-maven-plugin. So the file now configures five modules' test runs, one of them deliberately. Ingraphitron-lspandgraphitron-mcpit silently enabled parallelism those modules never declared, worth 17.5 s and 7.5 s respectively, which is a real win arriving through an invisible channel. Ingraphitronit put a secondjunit-platform.propertieson the classpath and produced the launcher warning that `graphitron’s own pom carries an explicit exclusion to prevent. Whichever way the parallelism question is settled, the provenance has to become visible: a contributor reading `graphitron-lsp’s pom and test resources today cannot discover why its test classes run concurrently, and a test there that is not thread-safe will start flaking with no local cause to find. (updated 2026-08-25, created 2026-08-20) -
R637graphitron:dev reports schema-load failures as infrastructure: ASchemaProblemthrown by graphql-java at schema assembly is an author-correctable failure, butgraphitron:devreports it asfailed (infrastructure)with a full stack trace.DevMojo.runGeneratorPasshas author-facing catch arms only forValidationFailedException(validator verdicts, rendered byWatchErrorFormatter) andSchemaParseException(mid-edit syntax, one attributed line); aSchemaProblemescapingGraphitronSchemaBuilder.buildBundle(which only rewrites federation errors intoValidationFailedExceptionand rethrows the rest) falls into the genericRuntimeExceptionarm. The batch goal already gets this right:AbstractRewriteMojocatchesSchemaProblemand formats it throughSchemaProblemDiagnosticinto a clean author-facing message. (updated 2026-08-12) -
R615init.sql documents the live idreffixture DDL as serving deleted shim tests:graphitron-sakila-db/src/main/resources/init.sqlopens theidreffixtureschema with a header block framed on the deletedIdReferenceFieldsynthesis shim. The stale part is narrower than the block reads: its stated purpose and its per-FK shim-versus-column-lookup prose have lost their referent, while the sentence aboutstudieprogramcarrying__NODE_TYPE_ID/__NODE_KEY_COLUMNSdescribes a mechanism that is untouched and load-bearing. A rewrite that took the whole header as false would delete live facts along with the dead ones, so the three claims are graded separately here. (updated 2026-08-09) -
R781intent_field_column_table costs 151 seconds for 116 rows, and it is the inlining:intent_field_column_tabletakes 151 seconds to return 116 rows against a realistically populated store, and nothing under it is the reason. Measured by the acceptance dry run of the store-performance skill: a real capture of the sakila example’s 4111-line schema against the sakila jOOQ catalog, the relation timed in isolation, then each of nine children timed the same way. The children are all cheap.intent_argmapping_key_column_candidateis 0.75 s,intent_argmapping_binding_leafandintent_field_accessor_hopare about 27 ms, and the other six are between 0 and 7 ms with four of them empty. So there is no expensive child to reach for and nothing underneath it worth registering, which is the push-down lever ruled out by measurement rather than by taste. (updated 2026-08-21) -
R614lookupKey.adoc claims a per-input-field shape the Query side rejects:docs/manual/reference/directives/lookupKey.adoc’s Constraints list states: "@lookupKey` on an individual input field applies only to that field; the rest of the input behaves normally." On the Query side that shape is rejected outright. A@tableinput argument whose leaf fields carry@lookupKeyindividually classifies asUnclassifiedFieldwith aDirectiveConflictreading "@lookupKey on a mutation input field is no longer supported; remove it … On Query-side @table input args, move @lookupKey to the surrounding ARGUMENT_DEFINITION instead". The preceding sentence in the same bullet is correct and measured:@lookupKeyon the argument does promote every leaf scalar of the input to a key, and all of them ride the VALUES join. (updated 2026-08-09) -
R830meta_relation_reference costs 153 ms, and it is the key-constraint projection inlined at both namings:meta_relation_referenceis the relation that answers which declared foreign keys cross which family boundaries: one row per foreign key the schema declares, naming the referencing relation and the referenced one, each with the family the census places it in. Its readers are the generated schema reference, the normalization-crossing gate, and the editor tooling’s schema surface. One read of it costs 153 to 166 ms. (updated 2026-08-25) -
R540mutation.adoc documents UPSERT the generator refuses at dispatch:docs/manual/reference/directives/mutation.adocstill teaches@mutation(typeName: UPSERT)as a working verb: it appears as a documented enum value withON CONFLICTsemantics and row-identification prose (about nine mentions on the page). The generator refuses the verb at classifier dispatch with "not yet supported; the conflict-target’s uniqueness and the bulk-UPSERT cardinality story are not designed" (FieldBuilder’s DML-kind dispatch), the example schema carries no upsert field, and the `directives.graphqlsdoc-comment already states the refusal. A reader following the manual authors a schema the build rejects. (updated 2026-07-26) -
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-written pipeline guard inGraphqlHttpHandler(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) -
R780srp names the reviewer as the disqualified party: the session grep takes the first ID in the body, not the trailer: Thesrpskill resolves the reviewer-rule guard’s disqualified party by grepping the last commit’s whole message for a session ID and taking the first hit:git log -1 --format=%B "$sha" | grep -oE 'session_[A-Za-z0-9]+' | head -1. Thehttps://claude.ai/code/session_<id>trailer is the identifier the guard compares, and it is the last line of a commit message, not the first match in it. Any commit whose body mentions another session ahead of its own trailer resolves to the wrong party, and the emitted review prompt then carries that wrong ID as a stated fact the next reviewer is told not to re-derive. (updated 2026-08-21) -
R553srp resolves one disqualified session where the guard means every session with a trail on the draft: Thesrpskill’s Spec-stage step 4 resolves the disqualified party withgit log -1on the spec file, yielding one session, the tip committer, which it stamps into a singularDisqualified session ID:token.roadmap/workflow.adocstates the guard’s purpose as fresh context ("a reviewer session with no prior reasoning trail on the work spots design problems the authoring or implementing session has already rationalized away") and explicitly anticipates a reviewer landing revisions, which "disqualifies that session from approving the resulting revision; another session must sign off". Those two readings come apart on the encouraged path: once a reviewer session commits a revision pass, the session that wrote the draft is no longer the tip committer, so the skill omits it and the emitted prompt affirmatively tells a disqualified author that it is not the disqualified session. That is worse than saying nothing, because step 4’s own rationale is that "the next reviewer applies the rule by ID, not by re-deriving it". (updated 2026-07-27)
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.
-
R403Rethink and reintroduce @tableMethod: the directive and its machinery are removed; reintroduction is a fresh design, not a re-advertising edit. Not a release priority -
R404Reintroduce @sourceRow documentation when it enters the supported surface: gated on @sourceRow re-entering the supported surface; not a release priority
Concept explainers
Intuition-first background pages for dense or recurring roadmap concepts, rendered as interactive HTML. The pages are authored by hand; this listing is derived from roadmap/concepts/*.html and must not be edited directly.
-
Consumer-derived input tables (backs R97)
-
Flattened selection sets and shared result keys (backs R499, R500)
-
Multi-tenant fan-out (backs R46)
-
Operation-divined tenant routing (backs R45)
-
Parent-holds-FK correlation in polymorphic child fields (backs R481, R487)
-
Per-participant join paths for polymorphic child fields (backs R458)