The fact layer LLMs are missing
Why a model that has read your entire codebase still does not know what a row means.
The gap nobody logs
Ask a coding agent what a subscription row means in your system and you will get a
confident, fluent, well-structured answer. Some of it will be right. The parts that are wrong will
not be marked, because the agent has no way to know which parts those are — and neither, on a first
read, do you.
This is not a reasoning failure. The agent did the only thing available: it read the schema, the migrations, a few service classes and the tests, and inferred a domain consistent with all of them. The trouble is that many domains are consistent with the same code. A nullable column is consistent with this is genuinely optional, with this is mandatory but was added later, and with nobody ever decided. A unique index tells you the database refuses duplicates; it does not tell you whether that was a business rule or a performance fix that has since become load-bearing by accident.
The information required to tell those apart existed once. It was in the conversation with the person who understood the business. Then it was compressed into DDL, and the compression was lossy, and the lossy artifact is the only thing anyone kept.
We have spent thirty years asking machines to infer a conceptual model from a logical one, when the conceptual model was the thing we should have written down.
Why retrieval does not close it
The obvious response is to give the agent more context: index the wiki, embed the tickets, add a retrieval step. This helps, and it does not solve the problem, because it is answering a different question. Retrieval finds text that is similar to the query. Nothing in the pipeline establishes that the retrieved text is true, current, or complete.
Three failures follow, and they are structural rather than fixable with a better embedding model:
- Stale beats absent. A design doc from 2021 describing a rule that changed in 2023 scores just as well as a correct one. The retriever has no notion of which is current; the generator has no reason to doubt it.
- Silence looks identical to permission. If no document mentions that a customer may have at most one active subscription, nothing is retrieved, and the agent proceeds as though the constraint does not exist. An absent rule and a permitted action are indistinguishable.
- You get chunks, not a domain. Retrieval returns the three passages nearest the query. Constraints, though, interact: a mandatory role plus a uniqueness constraint means exactly one; drop the mandatory and the same fact type means at most one. Reasoning about a rule you retrieved without the rule you did not is how you get an answer that is locally plausible and globally wrong.
Prose documentation has the same problem in a slower form. It is written by hand, so it drifts; it is written for a reader who already has context, so it omits; and it has no relationship to the running system, so nothing ever fails when it becomes false.
What an elementary fact adds
An elementary fact is the smallest statement that says something about the world and cannot be split without losing information. Person works for Company. Person has Skill. It carries no attributes — which sounds like a limitation and is in fact the entire mechanism. Because there is nowhere to hide a property, every decision about that fact has to be stated as an explicit constraint on a role.
Compare what each artifact commits to, for the same piece of domain:
| Artifact | What it says | What it leaves open |
|---|---|---|
person.company_id int NULL | There is a link, sometimes absent | Whether absence is legal, whether two are possible, what the link means |
class Person { company?: Company } | The same, in another language | All of the above, plus whether the optionality is a rule or a migration artifact |
| “People belong to a company” in a README | An intention | Everything that matters, and whether it is still true |
| Each Person works for exactly one Company. | Mandatory, unique, named, directional | Nothing — and a domain expert can reject it out loud |
The fourth row is not a nicer comment on the first three. It is generated from constraints in a model, so it cannot drift from them; it is checked against sample data, so it cannot contradict the examples; and it maps down to the first row mechanically, so the column is a consequence of the rule rather than a place the rule went to die.
Six properties that happen to suit a language model
Fact-based modelling was formalised in the 1970s and 80s — NIAM, then ORM, then Halpin's ORM 2 — for an entirely human reason: a schema that a business expert cannot read is a schema nobody validated. The properties that make it readable to that expert turn out, without anyone planning it, to be the properties an LLM needs.
- It is natural language. Verbalization is not documentation generated beside the model; it is the model rendered in the format a language model handles best. No diagram to interpret, no notation to learn, no vision step to misread.
- Every rule is stated. Mandatory, uniqueness, frequency, value, subset, exclusion, ring and cardinality constraints are all first-class and all verbalized. An omission is a real signal, not an ambiguity.
- Nothing is implied by an attribute. There are no attributes, so “one email per person” is a constraint someone drew rather than a consequence of where a field was typed.
- Facts read both ways. Every fact type has readings from each role, so the domain answers a question asked from either side.
- Examples are attached. Sample populations are stored with the model and read back through the readings. The rules come with instances, and the instances are checked against the rules.
- It is compact. A domain that is six thousand lines of DDL is a few hundred sentences as facts. It fits in a prompt whole, which removes the retrieval step and the partial-context failure along with it.
The part that makes it safe
Handing an agent a good description of your domain makes it faster. It does not, on its own, make it trustworthy — the agent will still propose changes, and some will be wrong. What closes that gap is that a conceptual schema is checkable: the same file the agent reads can be validated, diffed and enforced.
So the loop has a brake in it. The agent proposes a change to the model. The validator rejects it if the result is not well-formed, or if it contradicts the sample facts already recorded. What survives comes back to a person as one sentence in a pull request:
- Each Person works for at most one Company.
+ Each Person works for exactly one Company.
That line is reviewable by someone with no ORM training and no patience for JSON, which is the only reason the review will actually happen. Speed comes from the agent; safety comes from the fact that the artifact it edits is one a human can audit at a glance.
Write the layer down
None of this requires believing anything in particular about how good models will get. If they get much better, they will still not be able to distinguish a business rule from a coincidence in your data, because that distinction is not present in the data. It is a fact about your organisation, and somebody has to say it.
Fact-based modelling is how you say it in a form that survives — precise enough for a machine, plain enough for the person who knows the answer. Factum keeps that form in a text file in your repository, verbalizes it, validates it in CI, and serves it to your agent over MCP.