Every table I have ever written starts from the same quiet assumption: that there is one right answer and my job is to store it.
bedrooms: 3
. Done.
That assumption survives right up until two sources tell you different things and both of them have a reason to be believed. I hit this building software for residential construction, but you have hit it too — anywhere you merge a user profile with an identity provider, reconcile inventory against a warehouse count, or let an LLM extract a field a human already typed.
The usual fix is a priority order. Measured beats stated beats whatever the API returned. It works for about a week.
Here is what I do instead, and the two design decisions that made it hold up.
The first change is small and it changes everything downstream. A row is not
{ bedrooms: 3 }
it is
{
field: "bedrooms",
value: 3,
source: "assessor-record",
evidence: "RECORD",
state: "unverified"
}
Three sources saying "3 bedrooms" are three rows, not one row written three times. Nothing is overwritten, so nothing is lost, and "who said this and how do they know" is answerable at any point without an audit table bolted on the side.
Every claim carries an evidence grade:
MEASURED > STATED > RECORD > MODELED
Measured is something the system observed. Stated is a human asserting it. Record is an institutional file. Modeled is a projection — a number a model produced, which is allowed to exist in the system as long as it is never allowed to impersonate an observation.
This is the part I got wrong first, and it is the interesting part.
If you rank sources globally, a homeowner typing "it's a ranch" outranks the town assessor on the number of stories, because the homeowner is a human making a direct statement and the assessor is just a file. That is obviously nonsense. But the flat ordering has no way to express why it is nonsense.
The fix is to scope authority to a domain rather than to a source:
| Domain | Authority | Because |
|---|---|---|
| Legal / valuation facts | Assessor record | It is the legal instrument |
| The visible envelope | Vision pipeline | It is looking at the building |
| Intent and recent work | Homeowner | Nobody else can know it |
Now the assessor wins on stories, the vision read wins on what the siding actually is, and the homeowner wins on "we redid the roof in 2023" — and each of those is a defensible rule rather than a coincidence of ordering. Inside a domain, evidence grade breaks the tie.
One more rule that took a while to arrive at: a standoff is gated on evidence grade, not rank. A high-authority source with weak evidence does not automatically beat a low-authority source with strong evidence. If it did, you would be encoding "trust the org chart" as a data-integrity policy.
Every field resolves to a state, and the states are the API:
| State | Meaning |
|---|---|
confirmed |
|
| Independent sources agree | |
reconciled |
|
| They disagreed; resolved by domain authority + evidence | |
single-source |
|
| Only one source. Recorded, and flagged as such | |
conflict |
|
| A genuine standoff. Surfaced, not hidden | |
unverified |
|
| No verification stamp yet |
conflict
is the one that earns its keep. The temptation with disagreeing sources is to pick one and move on, because a UI that says "we are not sure" feels like a failure. It is not. Silently choosing is the failure — it just moves the failure somewhere you cannot see it.
Suspect claims get demoted, not deleted. Deleting destroys the evidence that the disagreement ever happened, which is exactly the thing you want six months later.
The second decision is the one I would port into almost any system I write from now on.
Records can be signed — in my case by two parties, the homeowner and an internal reviewer. The naive version of this is a boolean:
{ verified: true, verifiedBy: "...", verifiedAt: "..." }
That boolean is a lie the moment anyone edits the row. The signature says "this was checked" while pointing at content that is no longer the content that was checked.
So the stamp is bound to a hash of the content it signed:
{
entryHash: "a3f9...",
verification: { by: "homeowner", at: "...", signedHash: "a3f9..." }
}
Change the value, the hash changes, signedHash !== entryHash
, and the verification lapses automatically. Not "is flagged for review by a nightly job." Lapses, as a property of the data, at read time, for free.
You cannot quietly edit a verified claim and keep its stamp. That single property is the difference between a record that is auditable and a record that is merely editable, and it costs one extra column.
It also gives you a review queue for free, ordered by how much attention each thing needs:
quarantined › lapsed › unverified › awaiting-stamp › unstamped › stamped
Nothing goes unreviewed just because nobody happened to touch it.
Honest accounting, because the whole point of the design is honest accounting.
Reads are more expensive. You are resolving a view over claims instead of selecting a row, so anything hot needs a materialized current-state projection, and now you have a cache invalidation problem you did not have before.
Writes are chattier and the storage grows monotonically. You are keeping the losers.
And the UI has to be able to say "these two disagree," which is a design problem most interfaces are not built to handle and which you will have to solve for real rather than hand-wave.
I think it is worth it in any domain where being wrong is expensive and where you will eventually have to explain how you arrived at a number. Where the cost of being wrong is low, a boolean and a last_updated
are fine and you should use them.
This is the Master Ledger inside ML Systems, a construction technology company I run in Rhode Island. It is shipped and working — the ledger, the two-key verification, and the lapsing signatures are all live in the app on both app stores.
I should also be clear about what is not proven: the construction loop the ledger feeds is modeled, not measured. We label every claim in our public repo MEASURED
, MODELED
or ASPIRATIONAL
for exactly that reason, and I would rather tell you which is which than let you assume.
The design docs are open, including the ledger, the ontology that governs how the claims compose, and how a whole house gets compressed into a canonical model: github.com/MLSystemsRI/ml-systems-public
If you have built something similar — especially if you found a cleaner way to express domain-scoped authority than a lookup table — I would genuinely like to hear it.