ID |
|
|---|---|
Status |
Backlog |
Bucket |
architecture |
Priority |
4 |
Theme |
classification-model |
Created |
2026-08-19 |
Updated |
2026-08-19 |
Run-record families: committed command rows and the emitted-unit census land in the store
The store answers what the schema means, and after
roadmap/planners-read-facts-emitters-read-commands.md it 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 in
roadmap/list-ordering-invariant-enforcement.md is 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.
Shape
A run-record stratum, written downward by the tier that owns the rows, at the tier’s own cadence:
-
The plan’s committed command rows, written after
EmitPlan.producecommits them. Keyed by coordinate plus the glue keys the relations already declare; javapoet types stay out, exactly as the plan itself refuses them. -
The render’s emitted-unit census, written by the render fold: coordinate to emitted class, method, file. The fold’s closure invariant already holds this mapping in hand (every emitted method is the render output of exactly one committed command), so the writer is a fold over data the shell has, not a parse of the generated tree.
The cadence precedent is javac_: a post-capture family with its own writer, whose graph partition
capture clears "because its rows describe an emitted tree the run is about to replace"
(graphitron-model/src/main/resources/no/sikt/graphitron/model/graphitron-model.sql, header). The
emitted-unit census is the second family whose corpus the run itself produced; the fact model
(docs/architecture/explanation/fact-model.adoc) names javac_ as the only one today.
The emitted-unit census is a corpus read, not a written record
This half was specified above as a fold over what the shell already holds, "not a parse of the generated tree". That was decided before the write-direction boundary existed, and it is the wrong side of it. Two things settle it.
Positions. The use case is jumping from a schema coordinate to a list of candidate sites in
generated code, which needs a target file, line and column, and a description telling the
candidates apart. The render fold cannot know a line or a column: a position exists only once a
TypeSpec becomes text, and JavaFile.writeToPathReporting renders and writes in one step. Getting
positions from the fold means teaching graphitron-javapoet to report per-member offsets as it
emits, a new contract in a library whose value is knowing nothing about meaning. Read from the
emitted tree instead and SourceWalker already reports 1-based line and column per declaration.
The boundary. A corpus fact can always be gathered; a conclusion can only be reported. The model can go read any corpus, the emitted tree included, so it should never accept a corpus fact from a consumer. It cannot go read a decision that existed only in the memory of the tier that reached it. That sorts this item’s two families to opposite sides: the committed command rows are a conclusion and are reported downward, fenced by the no-upward-read rule below; the emitted-unit census is a corpus fact and is gathered. This item already half-says so, calling the census "the second family whose corpus the run itself produced" and then writing it from memory anyway.
How the census is gathered
-
The generator writes provenance into the file it emits.
addJavadocexists onTypeSpec,MethodSpecandFieldSpecand is already used by a dozen generators, so this is established practice rather than a new capability. A file is the generator’s own output channel, so writing to it breaks no rule: the store reads what the generator wrote exactly as it reads what the consumer wrote. -
The marker is
@see "graphitron:<KIND> <Coordinate>", with a human sentence in the prose above it. The form was chosen by testing the alternatives against a real javadoc run, because we emit into consumer projects and a marker that breaks their build is not available. The results are in the table below.
| form | default javadoc |
-Xdoclint:none |
-Xdoclint:all |
|---|---|---|---|
a custom block tag |
error |
warning |
error |
|
error, reference not found |
error |
error |
|
silent |
silent |
silent |
a prose line |
silent |
silent |
silent |
A custom tag is unavailable and cannot be waived: -Xdoclint:none downgrades the unknown-tag report
to a warning rather than silencing it, so the check belongs to the standard doclet rather than to
doclint and no flag turns it off. {@link} is also wrong on the merits, a schema coordinate not
being a Java program element, and the reference gate this repository runs on its own sources is the
check that catches it. A prose line passes but, placed first, becomes javadoc’s summary sentence and
so every generated member’s editor tooltip; placed last it is a magic trailing line with nothing for
a decode to anchor on. @see "quoted" is javadoc’s own defined form for a reference to something
outside Java, which is what a coordinate is, and it renders as a see-also entry rather than
displacing the summary. GraphQL names are [_A-Za-z][_0-9A-Za-z]*, so no coordinate can contain a
quote and there is no escaping hazard.
The rest of the shape:
-
@seeis repeatable, which is the grain. A shared site serving several coordinates carries one tag per coordinate; a coordinate served by several sites is several members carrying the same marker. Both directions are rows, which is what makes a list of candidates the natural read. -
The graph name stays out of the marker. A generated file is attributed to its graph by joining
java_file.source_roottostore_graph_output.output_directory. The graph is a fact about where the file sits, not about the file, and spelling it twice mints a value that can disagree. -
The marker reaches the store with no new machinery.
Trees.getDocCommentreturns a doc comment with block tags intact andSourceWalkeronly whitespace-strips it, so the marker lands verbatim in thejavadoccolumn thatjava_class_declaration,java_method_declarationandjava_field_declarationalready carry. Verified against a probe file rather than assumed. So the census is a decode over a transcription that already exists, which is the same shape thegraphitron_relations have over thegraphql_directive applications, and it is stratum two. -
The kind is closed, the description is rendered. A free-text description in the store is unqueryable and undiffable. The marker carries a CHECK-constrained kind naming what the site is (the projection, the fetcher, the mapper, whatever the emitters actually distinguish) and the human sentence is rendered from the kind plus the class and member. That gives the MCP a filter and gives a gate something to assert coverage against.
-
The census keys on the coordinate relation rather than on a rendered coordinate string, per the store’s key discipline, and takes one foreign key to it rather than three relations or one relation with a nullable field-and-argument tail.
What this costs, knowingly
A documentation tag is a published channel: a consumer who generates and publishes javadoc will
carry See Also: "graphitron:PROJECTION Film.title" in their rendered output, and nothing suppresses
that while the marker stays in a comment. The only form that avoids it is an annotation, which costs
a compile-scope dependency on a graphitron artifact plus a walker change plus a schema change, since
SourceWalker records no annotations at all. Take the visible tag, as a decision rather than a
discovery.
Adding the marker changes every emitted file once, which collides head-on with the
graphitron-sakila-example identical-output check that
roadmap/capture-without-the-materialization-refresh.md carries as a delivery criterion. So this
lands after that item, not beside it.
What a corpus read cannot answer
Absence. A coordinate that emitted nothing leaves nothing to parse, and "emitted nothing" is indistinguishable from "never generated". So the anti-join this item was surfaced by, every list-shaped coordinate having a launcher row that carries an ordering, is answered by the command rows and not by the census. That is not a gap in the census; it is the two families being about different questions, which is the same reason they sit on opposite sides of the boundary above.
Freshness has the same honest shape either way: a workspace that never generated has no census rows,
exactly as it has no javac_ rows today.
The rule that keeps this compatible with the tier doctrine
The seam item bans the store serving plan-shaped views as planner inputs, and that ban is about
read direction, not about a record. The charter rule for these families: no tier reads a
run-record family upward. Planners read facts, emitters read the in-memory command rows the fold
hands them, and neither ever reads the record back; the record exists for gates, the MCP, the
language server, and cross-tier joins. A planner or emitter importing the record’s generated tables
is the violation the family comment states and a boundary test forbids, on the
StoreClientBoundaryTest model.
Freshness is inherent and honest: rows describe the last run, so an editor workspace that never ran
the plan has none, the same as javac_ today. Author-facing rules that must be fresh at edit
cadence (the declared-but-unlowerable rejection in the ordering item) stay fact-derived and do not
move here.
Sequencing
After or alongside the seam item’s per-family conversions, which are what make this cheap: once a producer derives its command rows from the store by SQL, writing them back beside the facts is one more statement at a grain the producer already owns. The ordering-invariant item does not block on this (its honesty half is fact-derived, its enforcement half can start as an in-memory fold gate), but its enforcement becomes non-regressable and its absent-row blind spot becomes queryable only once the command record exists.
Provenance
The emitted-code half was first listed as the "generated code catalog" dimension in
roadmap/knowledge-base-programme.md (R117), whose own fact-base note reconciles that the model
store, not the DuckDB projection, is where such a dimension lands. The command half was surfaced by
the ordering-invariant enforcement question (R677) during the spec discussion of the seam item
(R682, roadmap/planners-read-facts-emitters-read-commands.md), which deliberately keeps both
records out of its own scope.