docs(invariants): the forge-state acquisition model, decided once

Four queued checks (REMOTE-SYNC, PIN-DEAD, RELEASE-CURRENT, TAG-SIGNED) need
state a local clone does not have. STATE.md carried the mechanism as an open
question per check, which is how a two-call budget grows by accretion — asked
four times, answered four ways.

Decided once, from measurement:

- Refs go over the git protocol. `git ls-remote --tags` is anonymous, reports
  annotated-vs-lightweight for free via the peeled `^{}` ref, and does not
  share the API's rate-limit bucket: 20 rapid calls all returned, and an API
  call immediately after still got 200. Stated as "does not consume the API
  budget", not "unlimited" — the run was sequential and stopped at 20.
- Releases have no git equivalent, so RELEASE-CURRENT is the third API call
  and the only new one.
- The URL comes from the register (`forge` + `org` + name), never `origin`.
  `app-creator`'s origin is `ssh://git@…`; using it would work on this machine
  and fail for every other reader — the anonymity invariant failing silently.
- TAG-SIGNED answers "is it signed" (`git cat-file tag`, local), never "does
  the forge vouch for it" — the Verified badge needs a key registered there.

Also corrects the rate-limiter note: a steady 1 request/second tripped 429 at
around request 40, so the burst description was not the whole limit. Pacing
alone does not buy immunity; a smaller total does.

VERSION-DRIFT is recorded as approved, measured, and NOT built. The rule fires
on 15 of 18 tagged repos, twelve of them merely between releases. Its two
motivating cases are already answered elsewhere or need judgement no classifier
has. Recorded rather than left open, so the next session reads the measurement
instead of repeating it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015AkHEqTSr1k3HbeiHu1ggW
This commit is contained in:
Kjell Tore Guttormsen 2026-08-12 22:27:03 +02:00
commit 425e7e9bdf

View file

@ -113,6 +113,28 @@ would recreate, in data, exactly the drift this plugin exists to remove.
is a leaky bucket, not a fixed ban — a 20-25 request burst took up to ~15s
to fully drain. `fetchWithRetry` defaults to `retries: 5` /
`maxDelayMs: 8000` (23s worst case) to cover that.
**Burst is not the only way in** (2026-08-12): a *steady* 1 request/second
tripped 429 at around request 40, so pacing alone does not buy immunity —
only a smaller total does. Recovery measured at 35s drain plus 3s pacing:
12/12 through.
- **Two acquisition channels, and refs are not one of the API calls.** Four
queued checks need state the local clone does not have, and answering the
mechanism question once is the point — asked per check, it gets four
inconsistent answers and the budget grows by accretion. Anything answerable
from *refs* goes over the git protocol: `git ls-remote --tags` is anonymous,
reports annotated-vs-lightweight for free (the peeled `^{}` ref), and does
**not** share the API's bucket — 20 rapid calls all returned, and an API call
immediately after still got 200. Measured sequentially and only to 20, so it
is "does not consume the API budget", not "unlimited". Releases have no git
equivalent, so `RELEASE-CURRENT` is the third API call and the only new one.
**Derive the URL from the register (`forge` + `org` + name), never from
`origin`.** `app-creator`'s origin is `ssh://git@…`, which needs the
operator's key — it would work on this machine and fail for every other
reader, which is the anonymity invariant failing silently rather than loudly.
Signature status splits the same way: `git cat-file tag` reads the signature
block locally, while the forge's "Verified" badge needs a key registered
there — so `TAG-SIGNED` answers *is it signed*, never *does the forge vouch
for it*.
- **Codepoints, not bytes, not UTF-16 units.** Use `[...s].length`. An em-dash
exposes only the byte layer; astral characters expose the rest.
- **The reader decides a link's level, not just what is required.** Root
@ -168,6 +190,20 @@ would recreate, in data, exactly the drift this plugin exists to remove.
once, as a count (`WARN`), never as fourteen findings. Left unrecorded, that
`WARN` can never be cleared, which is the `titles` problem again; no
acceptance record is built until a repo actually needs one.
- **`VERSION-DRIFT` was approved, measured, and not built.** The proposed rule
— the manifest carries a released version string while HEAD sits N commits
past that tag — fires on **15 of the 18 tagged repos**, twelve of which are
simply between releases (ms-ai-architect 131 commits, the catalog 262). A
gate that fails a correct repository is the mechanism that gets gates
switched off, and this one fails almost all of them. The two repos that
motivated it are both already answered: guard's manifest claims `0.7.0` with
no such tag, which is an existing `VERSION-TAG` `ERROR` (and the same root
cause as org-ops' `PIN-DEAD` — the README pins `@v0.7.0` too); okf's case
turns on *behaviour-changing* commits past the tag, which no classifier reads
off git. Two legitimate conventions coexist here — bump-at-release and
bump-first — and nothing in a clone says which one a repo follows. Recorded
rather than deferred: a decision that is wrong is worse than no record, and
the next session should not re-derive this measurement.
- **No hook until the rule is precise.** A blocking gate that fails a correct
repository is the mechanism that gets gates switched off.