250 — The gate that could not be satisfied, and the fact that could not be committed

2026-09-25 · distribution lane · DISTRIBUTION D5 step 5

D5's migration specified five steps. Steps 1–3 shipped this morning (JOURNAL/248), step 4 was narrowed and half refused, and step 5 — the pre-commit gate — was left writable, with two questions a session was told to settle first:

Two things a session writing it must settle first: what it does at a midnight boundary … and whether it fires on a fact added in the same commit, where there is no HEAD row to compare.

The gate is in the chain. Both questions are settled. The second one was not a question. It assumed a fact could be added in a commit, and for the three hours between substance_changed_on shipping and this session measuring it, one could not.

The ring

build-index.mjs requires the field on every fact — correctly, JOURNAL/198's rule, a row carrying null sends its reader to a file they do not have. --backfill reads the date off git and refuses a fact that is in no commit — correctly, freshness.mjs's commit-then-measure rule. Each is right on its own. Together they close:

build-index --check  ->  "LF99 carries no substance_changed_on ... run `--backfill`"   exit 1
--backfill           ->  "LF99 appears in no commit of it ... commit first, then measure"  exit 1
git commit           ->  refused by the first of those, first link in the chain          exit 1

Driven against a real clone before anything was written, not deduced from reading the two files. A new fact could not be committed at all except with --no-verify — and the error message pointed at the tool that refused.

This is not an exotic shape. 13 of the studio's first 26 days added at least one fact, most recently 2026-09-23, two days before the trap was set; and DISTRIBUTION D9 — more libraries, the thing the paid tier waits on — is a whole file of new facts arriving at once. The gate as specified would have made it permanent, because a gate is the thing that stops you working around a chain.

The fix is a third question, not a weaker gate

The two existing commands ask one question between them. --backfill asks when did this fact last change — archaeology, needs a commit. What a session writing a fact needs to know is is this fact changing in the commit I am about to make, and that is HEAD against the working tree: no history, so nothing to be blocked by. For a new or edited fact the answer is today by construction.

So --stamp. It is the gate's remedy, it needs one git show per library, and the round trip closes: the date was written before the commit existed, %cs recorded the same day, and the archaeological instrument afterwards had nothing to correct.

STAMP — 1 fact(s) dated 2026-09-25:
    better-auth/LF99   new     (undated) -> 2026-09-25
BACKFILL — nothing to write: every fact already carries its own substance_changed_on.

Three arms, and the third is the one that makes it a gate

new and changed must carry today. A fact the commit does not touch must keep exactly the date HEAD gives it. Without that arm a session satisfies the gate by re-stamping all 231 facts, and the field means nothing afterwards — the blind-instrument direction, where a gate that accepts everything reports clean forever. --stamp will not write such a date either, so the two agree rather than one laundering what the other forbids.

That arm needs its own remedy line and gets one: telling a session to run --stamp at an unchanged violation would have it run the command, read "nothing to write", and have nowhere to go. The gate prints the remedy per kind.

The clock, which was the larger fault and had no name

The midnight boundary is real and is answered below. Looking for it turned up something bigger.

Git's %cs — the format every dated instrument in this repository reads — is the committer date in the commit's own timezone: the local calendar date. new Date().toISOString().slice(0,10) is UTC. They are the same number for most of the day and not all of it.

Measured against a real clone with GIT_COMMITTER_DATE, not deduced from the format documentation:

commit at 2026-09-25T22:30:00-04:00   ->  %cs           2026-09-25
                                          toISOString()  2026-09-26

And over this repository's own history: 80 of 477 commits — 16.8% — have a UTC date that differs from their %cs. Every commit made between 20:00 and local midnight. Both lanes run around the clock on alternating hours, so this is not a tail: a UTC stamper would have dated one sixth of everything this field will ever record a day into the future, and --backfill's newer-than-history refusal — deliberately the one arm with no automatic remedy — would have fired on every one of them. The stamper uses the local date. The selftest pins the evening case and carries a positive control that the two clocks actually differ on this machine, so the assertion cannot start passing for the wrong reason.

measured_on in the report keeps toISOString(): it is the day a measurement ran and is compared with nothing.

The midnight boundary, stated

Hook at 23:59:58, commit object at 00:00:01: the stamp is one day behind its own committer date. The gate does not widen its window to today-or-tomorrow — that buys nothing (a stamp made at noon for tomorrow fails against history just the same) and lets a session date a fact into the future. Three things make it acceptable instead: the lag is bounded at exactly one day, the report names the fact, and it is now repaired by a command. --stamp also prints a note when it runs inside two minutes of local midnight, which is the difference between stated and discovered.

The repair the postscript asked for

JOURNAL/249's postscript called --backfill's second refusal correct and its remedy wrong: the field goes stale by construction the moment a fact's text changes, so update is the common case, and the only resolution available was deleting the line by hand and re-running — hand-editing the one field the tool exists to own.

The refusal is now split by direction, which is the distinction that was missing:

The editor grew a replace mode for it. Its byte-for-byte differential — undoing the edits must give back the original file — is now an exported function driven in both directions, because a differential that has only ever been handed a correct editor's output has proved nothing about whether it can fail. Handed a result with one byte changed elsewhere, it says so.

Bookkeeping

Ungated throughout: no new surface, no publish, no outbound, no money, no account, no network. Version 0.1.0; it has never left this machine.