This page is history. It explains how the generator’s model arrived at its current shape, which
parts of that shape are settled conclusions, and which are the latest position in a sequence that
has already moved several times. It does not describe current behaviour: where this page and a
present-tense page disagree, the present-tense page wins, and where either disagrees with
graphitron-model/src/main/resources/no/sikt/graphitron/model/graphitron-model.sql, the DDL wins.
Read it for two reasons. The design decisions that look arbitrary from the outside are mostly answers to a specific failure that had already happened, and the approaches that were tried and retired are the project’s most expensive knowledge, worth stating once so they are not re-proposed by whoever did not read the item that retired them.
Work is cited by roadmap id. Ids of shipped items stay recoverable from roadmap/changelog.md,
which records discards as well as landings; the per-item files under roadmap/ are deleted when
an item ships, so they are named here as provenance rather than linked.
The vocabulary came first, March 2026
The rewrite’s first four weeks reached trunk as two commits. The commits behind them survived
only as unreachable objects and are now preserved on six branches, described at the end of this
section. The earliest of them, graphitron-rewrite-history, covers 2026-03-27 to 2026-04-04, and
almost none of it is code.
The first commit adds a document, docs/field-taxonomy.md, whose subject line is "Add field
taxonomy document with agreed vocabulary". The forty-odd commits that follow it over five days
edit that document and nothing else: renaming the field types, collapsing pairs of them that
turned out to be one thing, flattening intermediate levels out of the hierarchy, dropping suffixes
that carried no distinction. PassThroughChildField becomes NestingChildField, then loses its
suffix along with every other leaf. Union subtypes collapse into leaves. Two whole intermediate
types are removed from the child hierarchy in a single commit for being a level that classified
nothing.
The first code arrives on 2026-04-01, five days in, as empty sealed interface skeletons, and the commit after it adds validation tests for all thirty-five leaf types. The model was argued to a settled vocabulary before it was allowed to exist, which is why the sealed hierarchy arrived whole rather than accreting.
Two things in that week outlast everything built on top of them. The first is a discipline visible
in the commit subjects themselves: null and Optional error signals replaced by sealed
hierarchies, nullable condition and ordering slots replaced by variants, a reference path element
replaced by a sealed hierarchy, all within seventy-two hours of the first code. What later became
an axiom was a reflex from the first week.
The second is the key. On 2026-03-31, before any code existed, the plan document defined the container as a flat map keyed by the GraphQL specification’s own coordinate type:
GraphitronFieldinstances are stored in a flat map keyed byFieldCoordinates, the GraphQL-spec-standardised(typeName, fieldName)pair provided by GraphQL Java. This is the same type used as the key inGraphQLCodeRegistryfor data fetcher registration, so the two maps are parallel by construction.
The model was coordinate-keyed on its fifth day, and it still is: GraphitronSchema declares
Map<FieldCoordinates, GraphitronField> fields today. What changed was not the key but what the
key was for. Count the references and the shape is plain: FieldCoordinates appears zero times
across the model package’s leaf types, and twenty-six times in plan and seven in command. The
leaves never carried their own coordinate, so it served as an index you looked a leaf up by while
consumers dispatched on leaf identity instead. The coordinate addressed the model without being
what the model was about.
That is the precise thing the June normalization argument changes. It does not introduce the coordinate, which had been there from day five; it promotes it from lookup key to the thing facts hang off. The distinction sounds small and is the whole pivot.
The week has one more rhyme with what came later. On 2026-03-28 the taxonomy acquired a formal two-axis model, every leaf tagged with a creates or reuses pair crossed with a carries or terminates pair. It lasted about thirty-six hours, deleted the next day by a commit whose subject reads "replace academic scope interaction vocabulary with plain language". Dimensional decomposition of the leaf was proposed and rejected as too abstract in week one, then arrived again in May as three crossed enums and again in June as source, operation and target. The idea was not wrong in March. It had no substrate to live in yet, so it read as vocabulary rather than as structure.
How this record survived
The early history was collapsed four times, and each collapse discarded a layer of detail that only a rebase abandoned on someone’s disk still held.
The rewrite was developed as a long-lived branch alongside a legacy line that kept shipping, so
the branch was periodically rebased and squashed to stay reviewable. The vocabulary week folded
into one commit on 2026-04-04. Everything before 2026-04-14 folded again into a single
feat(rewrite): add rewrite pipeline commit when the module was extracted. On 2026-04-24 the
whole line, by then 566 commits, was squash-merged onto main as two commits, which is the form
trunk carries. Finally, on 2026-07-01, the move to the repository root (R182) rewrote the metadata
of every rewrite-line commit, which is why author dates and commit dates diverge across April to
June and path-based queries go blind before that date.
The last of those collapses documented itself. Its plan survives on the pre-squash branch as
graphitron-rewrite/docs/planning/plan-history-squash.md, and it carries something the commits
cannot: a table of sixteen files deliberately not carried forward, each with a reason. Most of
them are one mechanism, the dual-pipeline bridge that let the rewrite run beside the legacy
generator during migration: a @condition guard in the legacy source field, a generator ordering
fix, a class-reference fix needed only for the dual compile, a getTenantId() default on the
shared context, an @asConnection addition to the shared directives. The rewrite emits its own
context interface and injects its own directive copy, so none of it had a consumer once the two
pipelines stopped running together. The bridge was scaffolding, and the squash is where it was
removed rather than left to rot.
The reason the record is a thicket rather than a line is that the rewrite was built by several
concurrent sessions on parallel branches that were periodically merged, then rebased together.
Branch names surviving in the merge commits (validation-test-coverage-plan-r8PAv, -hENgE,
-EcOP7) show three siblings running at once in mid-April. Two of the preserved tips are from the
main line and two from the validation-test-coverage line, and they share no commit subjects at all
across the days they both cover.
The six preserved branches are complementary layers, not copies. Each was an independent rebase, so none contains another’s commits, and the work each uniquely holds is listed here.
| Branch | Covers | What only it holds |
|---|---|---|
|
2026-03-27 to 04-04 |
The vocabulary week commit by commit. 41 commits touch the taxonomy document; on every later line the whole week is one commit. |
|
to 2026-04-11 |
The dual-pipeline validate goal, where new-pipeline errors surfaced as warnings during migration, and the introspect plan. |
|
to 2026-04-11 |
Validation test coverage to parity, edge-case probes, and composite primary keys in |
|
to 2026-04-13 |
A snapshot of the validation-test-coverage line, including the merge of two sibling session branches into one another. |
|
to 2026-04-14 |
The same line a day later, and the granular form of everything tip-4’s first commit collapsed. |
|
2026-04-14 to 04-24 |
The final stretch, 595 commits over the legacy base, 320 of them documentation. Its
|
The chain is provably continuous rather than merely plausible, because two tree hashes match
exactly. pre-squash/tip-6’s tip and the first commit of `pre-squash/tip-4 have byte-identical
module trees, so the second is the collapse of the first with nothing added or lost. And tip-4’s
own module tree matches what trunk carries, once the squash plan document’s final revision is set
aside. Every commit from 2026-03-27 to the shipped module is therefore accounted for, either
granularly or as a collapse whose input and output are both preserved.
The busiest days of the entire project are in the last branch: 95 commits on 2026-04-23, 75 on the 19th, 71 on the 15th.
The month that set the rules, April 2026
Almost every discipline the project still runs on was written down in the three weeks between the first sealed skeleton and the squash. The rules arrived before the architecture they would eventually break.
On 2026-04-06 the work stopped pretending to be a refactor. The package rename from
no.sikt.graphitron.record to no.sikt.graphitron.rewrite says why in its own message: the
feature "was initially scoped around jOOQ record output but has grown into a full rewrite of the
generation pipeline. The new name is honest about this scope." What is now a multi-year programme
began as a plan to phase out a DTO layer.
The following day the classification boundary became a rule. A commit stripped every Table<?>,
Field<?> and ForeignKey<?,?> out of the taxonomy records, and the reference document recorded
the constraint: the schema builder and the jOOQ catalog are the only permitted holders of raw jOOQ
and graphql-java objects, and everything downstream works from plain strings, booleans and
primitives. That is, nearly word for word, the classification-leak rule the architecture review
still applies. In the same week, default arms were removed from sealed switches specifically so
that a new variant would break the build rather than be silently absorbed.
The rule that stated the whole programme came on 2026-04-11:
If a generator needs a piece of information that is not present in the taxonomy, the correct fix is to add a record component to the appropriate sealed type and extract the value in
GraphitronSchemaBuilder, not to import or access jOOQ or graphql-java types in generator code. This constraint enforces a complete taxonomy: every fact the generators rely on must be explicitly declared.
That is the earliest architectural use of the word fact in the project, four months before the fact store, and the sentence is already the argument for it.
Two days later the same idea acquired the name it still carries, in the message of the commit that
replaced SourcesRef with BatchKey:
SourcesRef.pkJavaTypeswas representation-thinking, it stored the types the developer happened to write, forcing generators to re-derive column information from the parent table’s PK anyway.BatchKeyis generation-thinking: key columns come directly fromparentPkColumns, carrying theColumnRefgenerators actually need to emit code.
Read that against the July measurement, which found the emit call graph "derived twice, once by the emitters that emit the calls, once by a hand-maintained switch that predicts them". It is the same complaint at a different scale. The whole later history is this one objection, forcing successively deeper answers: first move the value into the leaf, then move the decision into the model, then make the model a relation so nothing has to be re-derived at all.
The workflow arrived in the same window. By the squash there is a workflow.md carrying the state
machine this project still uses, Backlog through Spec, Ready, In Progress, In Review to Done, with
the reviewer guards stated as a different party from whoever last changed the artifact, and the
slug convention that names the work rather than the phase. It was not adopted later and applied to
the rewrite; it was invented alongside it, which is why roughly 320 of the final stretch’s 595
commits are documentation. Every status transition is its own commit, so the plan file is the
ledger.
And the growth was watched from the start. The roadmap of the time already required that "each new sub-taxonomy proposal comes with a one-line note on what distinct information it carries that a sibling cannot", with an instruction to audit at milestone boundaries which sub-taxonomies could collapse. It did not help. The model went from thirty-five leaves at birth on 2026-04-01 to about fifty by 04-11 and sixty-two by the squash, and kept climbing until August. That is the useful lesson in the whole month: the leaf count was never a discipline problem, so no amount of governance was going to hold it. It was a representation problem, and it took until August to be answered as one.
The sealed-leaf model, May 2026
The unit of knowledge was the sealed leaf. Every classifiable schema position resolved to exactly one variant of a sealed Java interface, and the model was the closed set of those variants plus the compiler-enforced exhaustive switches over them. Classification produced a leaf or a typed rejection; emission pattern-matched the leaf. The theory was settled in March and was not seriously questioned in this period, only extended.
It was right about a great deal, and most of it still holds. Decisions are made once, at the parse boundary. The compiler, not a convention, enforces that every consumer handles every case. Rejection is a typed variant rather than a string, which is why typed rejection is still the shape today.
What it was silent about was representation for more than one consumer. A leaf is an excellent answer when a single traversal exploits it and a poor one when the same knowledge has to be asked about from several directions, because the only way to ask is to walk it again. Three separate items name that cost within weeks of each other in May: R123 cites the project’s own principle by name, that two consumers evaluate the same predicate over a model field; R220 finds three near-duplicate shape predicates that drifted apart because each consumer re-derived its own; R218 finds binding provenance that is only ever re-inferrable because nothing carries it.
The cost of the leaf model itself was also named, and accepted, before it was understood. R258, on 2026-05-29, weighs two designs in writing and chooses the one it knows is worse for the model’s size, because under the theory in force the alternative reintroduced conditional slots: option (b) "avoids a leaf explosion but reintroduces a kind-conditional Optional slot", and the item leans (a) anyway. The leaf count grew deliberately.
Three items filed on a single day, 2026-05-08, seeded vocabulary that outlived everything around them. R119 introduced the schema coordinate as a first-class addressing scheme for tooling. R115 proposed capability as a stable namespace of what the generator delivers. R117 proposed a queryable model of everything the generator knows about itself, covering the parsed SDL, the classifications, the generated code, the runtime trace, the documentation and the roadmap. R117 also stated the constraint that kept the idea out of the architecture for three months: keep the store a projection, "rebuilt on every build, never a competing source of truth". The store the generator runs on today is that idea with that clause removed.
Four theories in seven weeks, June 2026
June is the fastest period of model change in the project and the hardest to reconstruct from memory, because the model was re-theorised four times.
It opens with the dimensional model of R222: the leaf’s identity decomposed into a cross-product
of three enums, carrier, intent and mapping, carried on one field type.
On 2026-06-16 an audit (roadmap/audits/2026-06-16-source-operation-target-reframe.md) argues
that mapping is not a real dimension, because it conflates the shape of the output with a
catalog-versus-Java polarity that belongs independently to each endpoint. It proposes instead that
a field is an edge from a source to a target, spanned by an operation. The audit prices its own
churn before recommending the change, noting that the pivot "re-vocabularises freshly-settled
work" and listing three items that had landed under the old vocabulary within the preceding weeks.
This is the period’s characteristic move: each reframe states what it invalidates before it is
adopted.
On 2026-06-18 two things happen in one day. R316 ships the reframe, deleting Carrier, Intent,
Mapping and SourceCardinality outright behind a test that fails if any of them reappears. And
R333 is filed, reframing the model again, from the leaf to the schema coordinate plus a set of
independently discovered facts. Its diagnosis is the origin of everything that follows, under a
heading that reads "Normalization: the leaf zoo is a denormalized view": composite fields are a
repeating group, a first-normal-form violation, and split or re-fetch addressing is a transitive
dependency on the parent query rather than a property of the child coordinate.
The same document, the same day, states the other half: functional core and imperative shell, with commands as the addressed output rather than a third concept, and the law that the core decides the entire emit while the shell renders and never assembles.
Both halves of the eventual architecture are written down on 2026-06-18, in one item. They ship seven weeks apart, because one of them was implementable immediately and the other needed a substrate that did not exist yet.
One day later R333 overturns its own founding claim. Its discovery log records that there is no complete query at codegen, since projection is assembled per request from the client’s selection set, so the unit the generator emits cannot be a query part. It is the method that emits query parts. That correction is where the method call graph, and therefore commands, actually comes from.
Commands, and the measurement that redirected them, July 2026
July made the functional-core half real. The command relation was proven across three unrelated families: R314 landed the re-entry family, R552 the condition family, replacing an entire layer of generated condition classes, and R541 the root SELECT family, where every root coordinate becomes a command row rendered by a total function that takes no schema, and roughly a thousand lines of inline SQL building left the fetcher generator. R549 is the umbrella these ship under. Each proof retired a legacy layer rather than patching it, which is what made the pattern credible.
Then, on 2026-07-26, the project measured its distance from the functional-core ideal it was
working toward (roadmap/audits/2026-07-26-fcis-command-layer-distance.md). Its headline is the
size of the gap.
| Package | Lines | Files |
|---|---|---|
|
29,837 |
73 |
|
12,690 |
124 |
|
124 |
2 |
Branch density in the shell ran about 1.6 times the model’s, and just over a thousand JavaPoet builder calls sat interleaved with 1,641 branches in the same files, so that, in the audit’s words, nothing structural stopped a decision from sitting between two code-block additions.
Two findings mattered more than the size gap. The one command mechanism that existed ran backwards: the shell called it and it returned a name, so commands were minted during rendering as an after-the-fact audit trail rather than produced by the core. And the underlying defect was not about purity at all. The audit states it directly: the emit call graph is derived twice, once by the emitters that emit the calls and once by a hand-maintained switch that predicts them, and the gap between the two had already produced a recurring class of bugs.
Two derivations of one relation is a normalization complaint. It is not fixable by moving effects to the edge, which is why the next step changed the substrate rather than the discipline.
The substrate changes, August 2026
The decision took eleven days and is unusually well documented, because it was spiked before it was adopted.
R595 and R589 were filed on 2026-08-04 with the open question stated as materialization: Java records over the existing classified-schema components, or a real embedded relational store. Two spikes ran on 2026-08-05. The first measured encoding of rich values, deterministic ordering and latency, and concluded in favour, with the reason that matters for the whole architecture: because derivation is the model, no classifier code holds hidden state, so purity "stops being a discipline and becomes a property". The second spike failed, productively. Table-valued functions could not drive derivations, and the parser rejected the alias declarations that schema-first code generation needed, so derivations stay ordinary SQL statements, insert-select strata and views including recursive ones, and code generation moved to a live database build driver.
The four documents of 2026-08-06 converted those verdicts into board state, including the
retirement of the standing dimensional-model umbrella to a lineage record. Then the store shipped:
R595 landed the graphitron-model module, its DDL and capture on 2026-08-08; R610 and R603
followed on 2026-08-09; R589 landed the derived claim stratum on 2026-08-10, closing on the line
that classification is a relation and validation adds facts.
R610 is the item that shows the move was necessary rather than merely tidy. Its motivation is federation: the same type name in two subgraphs is deliberately two different types, and type-name-only keys would merge them into a fiction or a constraint violation. Multi-graph keying is not expressible in a leaf model at all.
R563, shipping in the same window, is where the leaf population finally fell, from 51 output leaves to 40, with plan leaf references dropping from 156 to 124, after more than a year of growth.
What the record can and cannot show yet
The natural claim to make about an architecture like this is that it costs less per consumer, because the derivation is offloaded to the engine. That claim is measurable, and the honest result is that it is not yet established, while a weaker and more surprising version of it is.
The cleanest natural experiment predates the relational core. R398 landed the SDL lint engine over two days at the end of June, adding a substantial new fact source to a core that already had two shells. It cost 1,306 lines of new main source in the generator module. The two existing shells paid this:
| Shell | Main lines | What those lines bought |
|---|---|---|
|
6 |
The typed rule id on the wire. Three of the six are comment. |
|
0 |
Nothing. The findings arrived through the channel it already read. |
|
129 |
A separate new capability, finding-keyed quick fixes. |
The nine new lint rules reached both shells for zero marginal lines, because both already consumed
the BuildWarning channel and the new findings were more rows through it. Near-free propagation
of a new fact source to existing consumers is therefore a property of the shared model, and it was
delivered by the Java one, a month before the store existed.
Against that baseline, the relational substrate has one completed measurement and one structural limitation.
The measurement is R569’s cutover of the authored-claim conflict rule on 2026-08-11, which is controlled in a way architectural comparisons rarely are. The view landed shadowed, a parity test pinned it byte-equal to the live Java, and only then was the Java deleted. Behaviour was held fixed by construction. The rule’s Java went from 323 lines to 293 and from 26 branch points to 23, against 95 lines of view. Counting only the hand-maintained side, the substrate absorbed some work. Counting total lines it did not, and since SQL and Java lines are not comparable units, neither reading carries much weight on its own. It is one data point.
The limitation is arithmetic. Sixteen files in the generator module read the store, and two in the
MCP server. The language server reads none of them; it still reaches the model through the
rewrite/catalog projection seam. A claim about cost per consumer needs more than one consumer to
divide by, and the second one has not landed.
Two derivations are also implemented twice in the tree as this is written, deliberately, each pinned by a shadow test that will be deleted at its cutover. Any measurement taken against the working tree during a pivot prices that window into the architecture. The cutover commits do not, which is why they are the unit of measurement here.
So the position the evidence supports is narrower than the natural claim and points somewhere else. What the relational core has demonstrably changed is not the cost of a consumer but the status of the derivation: it became a thing that can be shadowed, compared against its predecessor, and replaced only once a test says the two agree. The architecture became experimentable on itself. Whether it also becomes cheaper per consumer is a prediction, and the experiment that settles it is to port one language-server feature off the projection seam and onto the store, and measure.
What the sequence shows
No step in this history retracts its predecessor.
Classifying once at the parse boundary survives as capture. The functional core survives as derived relations, which are pure by construction rather than by discipline. Sealed hierarchies survive both in the model and as the shape of the DDL’s own keys and constraints, which is why the honest description of the move is that the schema became the sealed hierarchy, not that types were replaced by tables. Commands survive untouched.
What changed at each step was the answer to a different question. Legacy Graphitron answered what to generate. The March vocabulary answered when to decide, and keyed the answer by coordinate. The sealed-leaf model answered how the decision is computed. The relational core answers what the decision is made of, and in doing so recovers the key the leaf hierarchy had buried.
Each was a correct and incomplete answer, and each was exposed not by argument but by a new consumer: the language server, the model context protocol server, the roadmap tooling and the documentation itself all needed to ask the model questions that a traversal-shaped model could only answer by being walked again. That is the reason the current architecture has multiple shells over one relational core rather than a single shell. The plural was earned by the failure mode, not chosen in advance.
Where the present is described
-
Graphitron Development Principles for the axioms in force.
-
Pipeline overview for the stages as they run today.
-
Typed rejection for the rejection taxonomy.
-
roadmap/changelog.mdfor the per-item record, including discards. -
roadmap/audits/for the dated working papers cited above. -
graphitron-rewrite-historyandpre-squash/tip-2,tip-3andtip-4for the pre-squash commits, which exist nowhere else. See "How this record survived" above for what each holds.