A schema that breaks a rule should say so in one place, at the spot the author edits to fix it, and every tool should read the same verdict. So a defect is a fact in the store: a row, written once by the stage whose facts it judges, at the written position of the offending directive. This page says how that relation is shaped, who writes it, and how to add a defect.
Why a defect is a fact
A defect is stored, not computed on each read. It sits at a written position, a file, line and
column that graphql_ast_entry holds, so an editor can underline it and the row goes when the
document stops writing that position. Everything that reports defects reads one relation,
graphitron_entry_defect_site, which adds the coordinate, type and field the position is about:
-
the editor, through the
diagnosticview; -
the MCP server, through the same view;
-
the build, through
EntryDefectVerdict, which turns every error-severity row into a located build error with the message the editor shows for it.
One relation and one verdict means the three cannot drift apart.
The store’s verdict is authoritative. validate and generate read it first, before the schema is
classified, and stop on it: a graph with an error-severity defect is not classified and nothing is
emitted, so an author meets the stored defect and never a second account of the same fault. dev
reports the same verdict and keeps running. A warning-severity defect shows in the editor and the
MCP server and does not stop a build.
The supertype
graphitron_entry_defect holds one row per defect at its written position: the position, the
code, and a detail string a message quotes. Its key is the position and the code, so one directive
breaking two rules is two rows.
A code names the rule broken, stated once; the position says where, and so in what context, it was
broken. A rule is not given a code of its own for each place it can be broken: a name claimed that
does not exist is one defect, ELEMENT_UNRESOLVED, whether a @routine at a root or an element
along a path claimed it, and the position it sits at is the place it was claimed (Alf, 2026-11-09).
graphitron_defect_type declares each code once:
| Column | What it says |
|---|---|
|
|
|
Whether the rule comes from the GraphQL specification, from graphitron, or marks something graphitron does not support yet. |
|
The code the editor shows. |
|
One sentence an author reads to understand the rule. |
|
The stage that writes the code’s rows, a |
Ownership
Each code is owned by the stage whose facts it judges. A gatherer’s anchor phase is a sequence of
stages, and meta_stage rosters them, each with its gatherer, its position in that gatherer’s order
and its class; the gatherer runs its stages from that order, and meta_stage_write states the
relations each one writes. A stage writes its codes as its last statement, once the facts they
judge are in: from a rule view of its own that EntryDefects writes for it, or directly where the
rule needs work a view cannot state, as the occurrence-depths stage writes the route defects of
the walk it takes. So no one statement reaches every relation a gatherer derives, and a stage’s rule
reads only what that stage and the stages before it wrote; one that read a later stage’s relation
would read the previous reading’s rows. A rule reading several stages' facts belongs to the last of
them. A stage whose own run has to stay lean writes no defects: its codes go to a stage of their own
straight after it, which writes nothing but those defects, as occurrence-depth-defects does for the
depth loop’s occurrence-depths (Alf, 2026-11-12). Every row carries touched_at, the instant of the
reading that found it. When its stages have
run, the gatherer sweeps the codes its stages own once, deleting the rows of
those codes the reading did not mark. Another gatherer’s rows are never touched.
The split is under way. graphitron_entry_defect_rule takes no new arm: a new code goes to the
stage whose facts it judges, and an existing one leaves with its family. occurrence-builds,
occurrence-depth-defects, outbox and entity-key-alternatives write their own; the
entry-defects stage, EntryDefects itself as the graphitron anchor phase’s last step, owns every
graphitron code no earlier stage writes yet, from graphitron_entry_defect_rule. A stage takes its
codes by writing them and moving their owner_stage, and the members a per-member defect names go
with it: occurrence-builds writes the node-id slot and container defects with the members a
polymorphic id’s names, from graphitron_occurrence_build_entry_defect_member_rule, and
entry-defects the @discriminate’s, from `graphitron_entry_defect_member_rule.
DefectOwnerGateTest holds this together: every code names a stage of a gatherer, a stage owns
exactly the codes it writes, every stage’s rule view emits only that stage’s codes and its gatherer
owns the view, the gatherer may read what the view reaches, and each Java writer runs in a stage.
StageRosterGateTest holds the order: the gatherer runs the stages its roster states, and no
stage’s rule view reaches a relation a later stage writes. StageWriteGateTest holds the writes:
every relation a stage wrote in a fact document’s capture, as store_graph_write_stage records it,
is one meta_stage_write declares.
Detail and subtype relations
A defect often has more to say than a code and a string. Where it does, its owner adds a relation
keyed by the defect’s key plus whatever the detail is about, with a foreign key to
graphitron_entry_defect ON DELETE CASCADE, and any code it carries is registered in
graphitron_defect_type like any other. When the owner sweeps a defect, its details go with it.
The detail goes in such a relation, not packed into the detail string. A detail no longer found under a defect still found is the owner’s to remove as well, by
marking and sweeping the detail rows or by rewriting them each reading.
A detail relation is stored when a reader asks for it, and not before. The condition overload defects once wrote each declaration they name as a row under the defect; nothing read those rows, so they went, and the declarations stand in the detail string until a reader needs them listed.
The call parameter defects are the example. PARAMETER_UNFILLED, PARAMETER_AMBIGUOUS and
PARAMETER_FILLED_TWICE are stated for every call, a @service method’s and a @condition’s alike,
and worded to hold for both. A defect’s key is its position and code, so one written directive that
leaves parameters unfilled in several calls, a field binding two tables or an input field consumed
under several fields, is one defect; which call, which bound table and which parameter is a row of
`graphitron_entry_defect_parameter_rule, keyed by the defect plus the occurrence, the call and the
parameter’s position, never the detail string. Only locating the written directive differs by kind (Alf, 2026-11-10).
A projection out of a node id splits its refusals by grain the same way. One that resolves to no key
column, several columns and none named, a name matching none, or a node id naming an interface or
union, is the written argMapping pair’s, reported where the pair is written whichever directive
wrote it. A parameter whose type is not the column’s is the call’s: NODE_ID_KEY_COLUMN_TYPE_DISAGREEMENT,
one rule whether the parameter takes the whole decoded key or one projected column (Alf, 2026-11-10).
The three parameter codes go to occurrence-depth-defects, straight after the depth loop that
settles every fill. This one is occurrence-builds', one rule over the node-id slots and their decode
shapes: a key column a pair projects into a parameter where no decode shape fits, its parameters
`graphitron_occurrence_node_id_slot_projection’s rows.
Three rulings narrow that rule (Alf, 2026-11-12). A whole decoded key lands only on a record or a
tuple: the node’s own record, a record supertype for an id of several node types, or a RecordN or
RowN matching the key. Handing a single-column key’s value to a parameter of the column’s type
without saying so is dropped; an author wanting one column projects it with argMapping, so
NODE_ID_KEY_COLUMN_TYPE_DISAGREEMENT only ever judges a projected column against its parameter, at
the @service or @condition reference. What a parameter receives of the id, the whole key or one
key column, is a fact of the landing, stated once beside the parameter the id lands on, and not of
the decode shape, which keeps a row only where a shape fits. And the node-id slot defects go to
occurrence-builds, whose rule view already reports the build’s defects, rather than to a stage of
their own: the stage is cheap, and one rule view per stage reads simplest.
Adding a defect
-
Declare the code in
graphitron_defect_type: severity, basis,lsp_code, a one-sentence statement. -
Choose its owner: the stage whose facts the rule reads, the last of them where it reads several. That stage’s gatherer must own or declare a dependency on every relation the rule reads.
-
Write it as the stage’s last statement, as a branch of the stage’s rule view or directly, marked with the reading’s instant and swept by the gatherer.
-
Add a detail relation where the defect has more to say, keyed and cascaded as above.
-
Pin it in a fact document: the schema, then the expected rows of each relation the case touches with the defect rows beside the fact rows, since the defect is half the answer for that field; and beside them a near miss that draws nothing. Where the document goes: What a test is built out of.
No build reader is needed: once declared with error severity, the code stops validate and
generate through the store’s verdict.
A defect raised on a valid schema is a regression, whatever its age and whether or not planned work already names it. The editor shows it to an author as a fault in a correct schema, so it is never left as a known gap: count what it hits, and fix it ahead of the planned work it was found during, as its own commit.
See The modeling discipline for what any stored relation owes.
What is not a defect
The classification walk still refuses schemas on its own, with typed rejections that reach the build as validation errors; Typed rejection describes them. Those are not rows here, and the walk runs only on a graph the store’s verdict accepts. A walk refusal that duplicates a stored defect is removed where it untangles simply; otherwise it is replaced in effect, since the verdict stops first, and goes with the walk.
Some rule families still judge in Java outside the defect family, the ones StoreDetections
gathers; their violations join the walk’s errors once the verdict has accepted the graph. They are
what remains to move into the family.