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 diagnostic view;

  • 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

severity

error or warning.

basis

Whether the rule comes from the GraphQL specification, from graphitron, or marks something graphitron does not support yet.

lsp_code

The code the editor shows.

statement

One sentence an author reads to understand the rule.

owner_stage

The stage that writes the code’s rows, a meta_stage row naming its gatherer, which sweeps them.

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

  1. Declare the code in graphitron_defect_type: severity, basis, lsp_code, a one-sentence statement.

  2. 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.

  3. 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.

  4. Add a detail relation where the defect has more to say, keyed and cascaded as above.

  5. 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.