Skip to content

0015 — Observation.subject_ref is checked, not enforced

Status

Accepted.

Context

Observation carries subject_type and subject_ref. Nothing in the storage layer connects them to anything. An observation can name a subject_ref that is a lot when it says plot, or a uuid that is nothing at all, and the log accepts it without comment.

The obvious fix is a foreign key. It does not survive contact with invariant 5.

What the offline default rules out

An observation routinely arrives before its subject. Two officers, two phones, two reconnection times, and the moisture reading syncs on Tuesday while the lot it describes syncs on Thursday. So:

A real foreign key is impossible. subject_ref would reference kernel.record_key, and out-of-order arrival breaks it on insert. Deferral does not help — constraints defer to the end of a transaction, not to the end of a sync window.

Rejection at ingest is worse than useless. It discards a valid record for a reason that stops being true a day later, and P6 says flag, never reject.

A stored subject_not_found flag is the trap decision 0012 already named. Quality flags are written once. The same two records syncing in the other order would carry different flags forever, and an immutable order-dependent flag is a worse artefact than none.

The distinction that settles it

Two different questions hide inside "is this subject valid":

Question Order-dependent Where it is answered
Does the subject exist? Yes Read time
Is it the kind of thing the observation declared? No Ingest, stored
Has the subject been retracted? Yes Read time

Once a subject is found, whether its type agrees with subject_type is settled forever. Nothing later changes a lot into a plot. That fact is worth writing down, and it is the one most likely to be an application bug rather than a sync artefact.

Decision

subject_type_mismatch is a quality flag, written at ingest, only when the subject is present and is demonstrably the wrong kind. Absence produces no flag.

Existence, actual type and retraction are resolved on every read and attached to the observation's RecordView as subject, batched one query per page, the same shape as custody, balance and fulfilment.

type_matches is deliberately three-valued. null means unknowable — either the subject has not arrived, or the declared type names nothing the log can hold. Collapsing that to false would report a missing record as an error and a present one as the same error.

An observation about a retracted subject is surfaced, not hidden. It remains someone's account of what they saw, and the fact that the thing they saw was later withdrawn is information about both.

Cost

One indexed lookup on the ingest path per observation. Ingest already reads the database for supersession, retraction and conversions, so this is not a new class of cost, but it is a round trip and it should show up first if ingest latency ever becomes a problem.

Finding for spec §13: region cannot resolve

SubjectType includes region, but regions are registry rows keyed by code and vintage. They are not records and have no uuidv7. subject_ref is z.uuid({ version: "v7" }), so a region observation cannot name its own subject. There is no value that would satisfy it.

Three ways out, none of which the kernel may choose for itself: regions become records, subject_ref widens to admit a registry key, or region comes out of the enum. Until then RECORD_TYPE_FOR_SUBJECT['region'] is null and a region observation resolves to type_matches: null rather than being quietly called broken. There is a test asserting this so the finding cannot be lost.