Every fact in the store is there because somebody decided it was worth capturing in our model. That is the first thing to understand about the store and the last thing to forget: nothing arrives because it was available. This page is the discipline that follows from it. The strict statement of the model is The fact model and the naming habit is Naming the row; this page is how a fact gets in.

Provenance is the only question that matters first

A fact has one of two provenances, and everything else follows from which.

Either it comes from a corpus outside the store, which is the SDL documents, the classpath, the jOOQ catalog, the configuration, the Java sources. Or it is derived from facts the store already holds.

That is the whole fork. It is the same cut as a gatherer’s capture phase against its anchor phase, asked about one relation instead of about the store.

A corpus is captured into a family shaped by the questions

When the facts come from a corpus, the discipline is not to mirror the corpus. It is to capture into a family model designed to support the questions we expect to ask against that corpus.

This is the part that is easy to get wrong, because mirroring the corpus feels neutral and is not.

The worked example is the classpath, and it is a model we have now finished replacing.

The jvm_ family described the classpath: classes, their supertypes, methods, parameters, record components, declared type references. It was an accurate description and it made no affordance toward what any of it is for. Nothing in it said whether a method may be named in @service, or whether it can serve as a condition. So every consumer that needed to know worked it out at query time, and worked out the same thing again, from the same rows, on every question it asked.

The code_ family was built the other way round. It started with a relation for each use site, one per place an author can name a method, so the fact a consumer wanted was a row rather than a predicate it had to re-derive. Only then was it normalized: supertypes extracted and shared facts lifted out, driven by the queries the generator, the LSP and the MCP server actually make against it.

Each use-site relation is its key and nothing else:

CREATE TABLE code_service_method (
  source_name VARCHAR NOT NULL, class_name  VARCHAR NOT NULL,
  method_name VARCHAR NOT NULL, descriptor  VARCHAR NOT NULL,
  touched_at  TIMESTAMP NOT NULL,
  PRIMARY KEY (source_name, class_name, method_name, descriptor),
  FOREIGN KEY (source_name, class_name, method_name, descriptor)
    REFERENCES code_method (...) ON DELETE CASCADE
);

Membership is the only thing the relation has to say, because what a method returns and takes does not vary by the directive that reached it. That lives once, in code_method. code_condition_method and code_external_field_method are the same shape for the other two sites.

So a family carries both the base facts and the aggregated and derived facts we know we need. code_type, code_method and code_method_parameter are read off the classfiles; code_type_element peels a generic signature down to what it delivers, code_throwable_supertype closes a hierarchy transitively, and the three use-site relations answer the question the census would never have answered. One family, both kinds, because the questions are what the family is for.

The jvm_ family was eaten away as this proceeded, from thirteen relations to none.

The classes jOOQ generates are classes like any other, and the code family captures them as code facts, so a relation that names a record class or a parameter type can key into it with a foreign key. Today the census reads the generated package’s classes without their members, and reading those is the same capture, not a new family. A nested class is no census class of its own, its methods reached through the class declaring it; where a census method returns or takes one, the census reads it by name for its members, as it reads the session handle, so a value a service hands back as a nested record is known like any other. Nothing holds a second reference to them by catalog name beside that. Which table a record class belongs to is a catalog fact, and the sql family states it in sql_table.record_class_fqn. Where a generated class is missing from the code family, the fix is capturing it there, not reaching for the catalog name instead.

A relation may land before its reader, when that reader is named. code_throwable has no reader in the tree yet; the editor’s completion for @error is the one it waits for, and it cannot complete against a fact nobody captured. What makes such a relation more than inventory is that its reader is named.

Normalizing is mostly mechanical once you know which facts you are holding: extract the supertype, lift out what does not vary, key it. Deciding which facts the model has to carry at all is the part nothing tells you except the questions, and it is the same decision this page opens with.

A missing fact is a capture question first

When a derivation struggles because a fact is missing, go back to capture before writing the rule. Capture may have had the fact all along. A gatherer often sees whole what a reader would otherwise have to put back together.

The worked example is a field’s path. A field’s path is the elements of its @routine and @reference applications, in the order they were written. The directive entries were shaped by the directives: one decode per application, with each element at its index within its own application, in graphitron_ast_reference_element_entry. No entry stated where an element sits in the field’s path. So every reader that needed that position rebuilt it:

  • the forward walk and the chain defects both needed the field’s first element, or the one after the element they stood on;

  • the inherited-table check, the column match and hover needed to know whether the field wrote a path at all.

Each one climbed from the element to its application to the field declaration, then ranked the applications with a window or asked whether anything was written before it. The same structure was re-derived from the same rows on every question.

The graphitron AST gatherer reads every directive on a field together, so the order was available at capture all along. Capture now states it once. graphitron_ast_field_path_entry holds one row per element, at its place in the field’s composed path, keyed by the field declaration’s position in graphql_ast_field_definition_entry. The readers' questions became keyed lookups:

  • the first element is place 0;

  • the next element is the place after;

  • a field wrote a path when it has a row.

The new relation is still an entry at the source document grain, keyed by where the declaration was written, and the graphitron AST gatherer marks and sweeps it with the rest of its path rows.

The ranking had also been a cost. A window blocks the graph filter a stage reads its rule with, so in a store holding many graphs every capture paid for all of them. Where the walk was still slow, the rest of the cost was the rule’s shape, a separate lesson.

The sign to watch for is a reader rebuilding a structure from many rows each time it asks: a rank, a window, or a "nothing was written before this one" test. That structure is usually a fact the corpus held whole. Capture it in the form the question needs, not the form the directives were written in.

Otherwise the facts are derived

When the inputs are already in the store, the fact is a derivation. Nothing has to be read again and nothing outside the store is consulted.

A derived fact is stored too: its owner’s stage writes it from a rule view into a relation with keys and foreign keys, and marks and sweeps it like any other. Stored at a grain an existing relation already keys, it is a derived relation; establishing a key no relation has yet is anchoring. A plain view is only for a trivial join and filter of stored relations, no view reads another view, and a rule view that grows complex is a sign a derivation was skipped upstream and belongs stored there. The fact model states the rule in full.

Gathering and deriving are kept apart

The discipline that makes the two provenances tractable is that a gatherer does not mix them. Put the base facts in the store first. Derive what you need afterwards, in the anchoring phase, which is the gatherer’s last step rather than a gatherer of its own. It is last because aggregating needs the base facts for the whole corpus, not for one document.

Base facts and aggregated facts answer different questions, and facts are additive. Gathering puts the base facts in; anchoring adds the aggregated ones beside them. Neither replaces the other, and a consumer reads whichever answers its question.

The other reason is that SQL is very good at deriving tables. Once the base facts are in the store, the aggregation is a statement rather than code.

The anchoring phase is where upstream families may be read

Gathering reads one corpus. Anchoring may read what earlier gatherers have settled, and that permission is declared rather than assumed: meta_gatherer_dependency names the upstream families each gatherer may reach. The code gatherer declares classpath-source and jooq, which is what lets a rule of its anchor phase key a method’s parameter to the catalog table the parameter names.

Upstream is the operative word. Reading a family that runs later does not work: best case the rows are not there yet, worst case they are there and subtly wrong.

A row in the roster can lag the design. While the classpath model is mid-transition it still carries a dependency on the family being replaced.

Entries and anchors

To make this concrete, take the graphql_ family. Its entries are at the source document grain, which is what lets the store hold the fact that two documents declare the same field:

graphql_ast_field_definition_entry   PRIMARY KEY (graph_name, source_name, source_line, source_column)
graphql_field                        PRIMARY KEY (graph_name, type_name, field_name)

Two documents declaring Film.title are two entry rows and one anchor row. The anchor is at the grain a consumer asks at, a key no entry has, and establishing that new grain is what anchoring is.

How a collision resolves is the decision that matters, and it differs from family to family. In this one the oldest file wins: the newer definition is the one a developer changed most recently, so that is where the error was introduced. Files are ordered by when they were last written, with the name as a tie-break, so the winner never depends on how a directory happened to list them.

Why an anchor is a table and never a view

Because a table can carry constraints and a view cannot.

Anchoring into a table buys a primary key, foreign keys, uniques and checks, which is how the store encodes its integrity rules in the database rather than in the code that fills it. None of that is available to a relation that is only a query. An illegal state is unwritable rather than merely unexpected.

That is the reason, and it is not a performance reason. A view can be fast and still cannot be an anchor.

Two useful consequences of the split. What an author got wrong is the anti-join between the two halves, and it is the only place a diagnostic can find it. And a corpus that does not build still has anchors, because they are derived from the entries rather than from a successful assembly.

What a stored relation owes

Anything the store keeps rows for owes three things.

An owner, which is the code that writes it. It is declared in meta_relation, so a new relation cannot arrive without somebody owning it.

A grain, said as a sentence. One row per field, one row per foreign-key hop. If the sentence does not finish cleanly the relation is holding two facts.

A mark and a sweep. This is what makes deletion work at all. A reading marks what it wrote and sweeps what it did not, so a thing removed from a corpus is removed from the store and ON DELETE CASCADE collects everything that hung off it. The sweep is the root of that collection: without it the cascade never fires, because nothing ever deletes the parent.

So a deleted row unwinds everything downstream: an entry, and a derived relation, references what it hangs off ON DELETE CASCADE. An anchor is the exception. It establishes a new key and may aggregate several sources, so it can outlive any one of them, and its foreign key to what it was built from is ON DELETE SET NULL. The row stands with its reference emptied, so a source going away flags every reading of it rather than silently deleting them, and the owner reclaims them deliberately on its next pass. Review a foreign key against this: cascade downstream, set null from an anchor.

A record of reading commits after what it vouches for

A source’s stamp says that the store’s rows of it are derived from these bytes, so a later reading may skip the source when the bytes have not moved. A capture commits gatherer by gatherer, so a run stopped partway keeps what its earlier gatherers committed. The stamp therefore commits with or after the facts it vouches for, never before them: written first, a stopped run leaves it vouching for rows nobody wrote, and every later reading skips the source on its word. Until the transcription has committed the stamp stays unknown, and unknown counts as changed, so the next reading reads the source again (Alf, 2026-11-10).

The source readings decide what changed and claim the source; the pass calls their vouch once the gatherers that transcribe the source have committed. A gatherer that records a reading of its own owes the same order.

Where it pinches

It would be dishonest to present this as enforced. Most of it is not.

It would also be misleading to present the store as though every relation in it followed the page. The intent_ family does not. It accumulated before this discipline was settled, it is the place rules went when nobody decided where they belonged, and it is dissolving: a rule that reads one family’s facts moves into that family, and the ones this work has reached have been deleted rather than improved. It is the second largest family in the store, so a reader will meet it early. Do not model on it, and do not take a shape found there as precedent.

New work builds new and clean. It reads no intent_ relation or view, even as a stopgap: a fact that exists only there is captured or derived properly first. And it adds nothing to the derivation gatherer, which is dissolving too, unless the change dissolves something there; a derivation step moves to its owner’s anchor phase. Both rules exist because a new reader or step in a dissolving family is one more thing to dissolve later. Before placing a change, check the owner in meta_relation and the writing stage of every relation it touches.

What holds, and holds well, is that a new relation cannot arrive without an owner and a grain. MetaDeclarationGateTest compares every observed relation against the declarations and a frozen roster of relations nobody has got to yet, and that roster only shrinks. If you add a relation, this is the gate you will meet.

What does not hold:

  • Nothing enforces the sweep. Not one gate checks that a stored relation’s rows are swept. One gatherer’s clear set is re-derived from its own source text and compared; the other lists are unchecked.

  • The ownership gate binds on declared views only, and within those, only where both ends are declared. A relation filled by hand-written jOOQ is outside it, because what such a producer reads is in no stored definition for a parse to find.

  • Nothing checks that a declared grain is the right grain. The gate compares the declared key against the actual primary key, which is two authored strings agreeing with each other. Writing the reader’s query first is what exposes a wrong key.

  • Roughly half the schema stands on the undeclared roster, so a claim about "every relation" is today a claim about the declared half.

The declaration is a gate rather than a mechanism: nothing that runs reads meta_relation. That is still worth doing, because the gate is what stops a relation arriving unowned, but it is not load-bearing at run time.

Where to go next

The fact model for the strict statement with each rule’s enforcing test named, Naming the row for the sentence a grain is written as, Defects as facts for the discipline applied to the defect relation, and Pipeline overview for where gathering and anchoring sit in a run.