Skip to content

One record per contract: dissolve ref/, and make server/spec/v1.md true - #863

Open
fujibee wants to merge 4 commits into
mainfrom
docs/one-record-per-contract
Open

One record per contract: dissolve ref/, and make server/spec/v1.md true#863
fujibee wants to merge 4 commits into
mainfrom
docs/one-record-per-contract

Conversation

@fujibee

@fujibee fujibee commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Affects existing users only as documentation. No behaviour change: the one code edit is a comment in scripts/internal/remote-sync.mjs, and three script headers change a path in a comment. Everything else is docs/ and server/spec/. This is the "something broken is repaired" kind — a reader following these documents was being sent to a removed model or a dead path.

What was wrong

One contract had four records, and the one with the authoritative filename was the dead one.

record what it claimed
server/spec/v1.md SUPERSEDED — do not build to this document
docs/design/remote-sync.md named by v1.md as "normative today" for the model
docs/spec/ref/stage-1-remote-sync.md named by v1.md as "normative today" for the sync contract — from inside ref/, whose README says do not cite one as the reason something is the way it is
scripts/internal/remote-sync.mjs:1472 "errorBody() … is the definition, since server/spec/v1.md is marked SUPERSEDED"

superseded is an ADR mechanism. CONTRIBUTING.md:52 states it: a later ADR supersedes an earlier one, and ADRs are immutable history. Specs get edited. Marking a spec superseded instead of correcting it is what let the implementation become the definition by default.

server/spec/v1.md

Rewritten from the implementation rather than patched paragraph by paragraph.

  • Endpoints. It declared eight. POST /v1/pairing/exchange and POST /v1/credentials/<id>/revoke do not exist anywhere in server/src; four that the server does register were absent. It now lists the ten server/src/app.ts registers. Cross-checked by extracting both sets and diffing: 0 difference.
  • Authorization. It required Authorization: Bearer on everything but health, in eleven places. The server's only mention of that header is redacting it from logs. The reason there is no credential — reaching the server is the permission, the network is the trust boundary — now sits where a reader meets it, along with the two things that rest on it: a repeat POST /v1/connect writing nothing, and the requirement not to expose a server to a network whose reachability you would not treat as authorization.
  • The pairing and per-device-credential section (112 lines) described a removed model. Replaced by the four registration and lookup endpoints, written from connectSchema, connectTeam, resolveTeamsByName and getTeamSnapshot — including 409 team-already-exists and why a credential-free route depends on it writing nothing at all.
  • Error table. Six codes belonged to the removed model (401 unauthenticated, 401 invalid-pairing-token, 403 forbidden, 403 credential-scope-violation, 409 pairing-token-consumed, 410 pairing-token-expired); three the server emits were missing (team-already-exists, cipher-profile-mismatch, team-name-match-limit-exceeded). It is now the set derived from every ProtocolError construction, and says explicitly what it does not cover, so 502/503/504 stop looking protocol-defined.
  • The forward-pointers naming two other documents "normative today" are gone. The two links that remain say what each document is for and state that neither restates the endpoint shapes.

ref/ is dissolved

ref/README.md — one blob copied into three directories — said "Nobody is building toward anything in a ref/ directory" and "Do not implement from a document under ref/. Do not cite one as the reason something is the way it is." Eleven documents lived there and were cited 34 times from outside, including from scripts/key.sh, scripts/remote-sync.sh, scripts/drivers/storage/sqlite-sync.sh and server/spec/v1.md. Making the distinction visible from the path did not stop anyone citing them.

Promoted nine, each measured against the implementation first:

document evidence it describes shipped code
docs/spec/stage-1-remote-sync.md all five storage_sync_* operations implemented; stage1-resync advertised by sqlite and not by jsonl, exactly as the spec's "optional capability" says
docs/spec/age-v1-profile.md age-v1 102 occurrences, X25519 8; its conformance vectors already sat outside ref/
docs/spec/read-state-synchronization.md read-state implemented and tested
docs/spec/retention-gap-resynchronization.md resync-required in remote-sync.mjs, storage.ts, errors.ts, plus three test files
docs/spec/server-opaque-envelope.md key_id / cipher / blob in protocol.ts, storage.ts, 001_initial.sql
docs/adr/0005-remote-sync-contract.md three of five decisions matched as written; see below
docs/adr/0006-composite-read-state-frontier.md frontier 45 occurrences
docs/adr/0007-stable-member-and-roster-identity.md member_id 321 occurrences
docs/design/adaptive-sync-catchup-v1.md implemented in remote-sync.mjs:2876-3135, which cites this document by name in three comments; 23 lines of tests

Deleted two. device-pairing.md (811 lines): pair-v1 and SAS appear nowhere in the tree and key.sh has no request/approve subcommand. remote-sync-dogfood.md (177 lines): it asked to be deleted once integration/remote reached main, which 17d0ba7 did, and its runbook teaches connect --token-stdin, a flag remote.sh no longer parses — one comment is all that survives of it.

Promotion is not a move

Statuses said proposed or dogfood — the vocabulary of the directory they sat in. Left alone, the documents would have been half-promoted: authoritative location, provisional label. The specs are now current and the ADRs accepted.

Two ADRs could not be marked accepted as they stood, because they still described the pairing and per-device-credential model the server no longer has:

  • 0005 — the device-credential paragraph is replaced by what a binding actually is (a local record with a revision, carrying no secret). "Onboarding cannot overstate history durability" kept its surviving claim — connect is not backfill — and lost the manifest-terminal-acknowledgement mechanism, which has no implementation. Its "key epoch" is written as key_id, the name the protocol actually uses.
  • 0007 — owner boundary, lifecycle and one rejected alternative restated without credential issuance or revocation.

Editing them is what the ADR rule permits while a document is proposed and unadopted (AGENTS.md: squash while single-owner and unmerged; the supersede chain becomes mandatory only after main). Flagging it anyway, because it is the judgement call in this PR — the alternative was to ship docs/adr/ containing paragraphs I had measured to be describing a removed mechanism.

stage-1-remote-sync.md also contradicted itself — line 37 said SQLite is the Stage-1 implementation, line 229 said SQLite and JSONL both implement it. It now names both and what each advertises.

The guard, and why it is the first commit

There was no link checker. That is why docs/spec/vectors/age-v1-vectors.json has been shipping "profile_document": "../age-v1-profile.md" — a path that did not exist, because the vectors already pointed at where the profile would live after promotion.

The first commit adds the checker and is red, naming eleven pointers before anything is repaired:

  • five in docs/adr/0003, which 75b8b1c moved out of docs/adr/ref/ without adjusting its relative depth — the last promotion out of ref/ broke five links and nothing noticed;
  • four from the age-v1 profile to its own conformance vectors;
  • the vectors' pointer back;
  • (a twelfth appeared mid-PR: the new pointer v1.md makes to the promoted spec, before the move.)

Landing it red first is the positive control. A checker introduced alongside the fix is green from birth and no one has seen it fail.

Both halves refuse to pass on an empty match, so a matcher that stops matching, or a renamed JSON field, reports itself rather than going quiet.

Verification

stopping condition expected measured
ref/ directories 0 0
tracked files under any ref/ path 0 0
residual ref/ references, path and name instruments 0 0
link checker green green
endpoints in v1.md vs registered in app.ts identical 0 difference

The move invalidated 38 relative links. All were rewritten by resolving each basename against the tree, with the checker enumerating them before and confirming after — no link was fixed by hand and hoped over.

A link checker is not enough on its own: 28 further references live in prose and codedocs/security.md and docs/security.ja.md cite the age-v1 profile by path with line numbers twelve times each, and three script headers name it in comments. No markdown link checker can see those, so they were swept with a second, path-and-name instrument and rewritten too. That is why the residual-reference row above is measured with both.

Suites run locally on this head (macOS): test_doc_links, test_type_registry, test_claude_template — 26 ok, 0 not ok. test_bin_agmsg, test_remote_setup_doc, test_storage_contract in CI's xargs bats form. docs/spec/vectors/verify-age-v1-vectors.mjs exits 0. test_remote_sync_engine passes. Local form is not CI's shard form.

Destination drift re-measured at commit time: origin/main is f7de22a and the merge base is f7de22a.

Nothing checked them. docs/spec/vectors/age-v1-vectors.json ships a profile_document pointing at a path that does not exist, and the vectors are what a second implementation reads first.

On this commit the check is RED and names eleven pointers: five in ADR 0003, which 75b8b1c moved out of docs/adr/ref/ without adjusting its relative depth; five from the age-v1 profile to its own conformance vectors, which sit outside ref/ while the profile does not; and the vectors' own pointer back. The following commits close all eleven.

Both halves refuse to pass on an empty match, so a matcher that stops matching, or a renamed JSON field, reports itself instead of going quiet.
It carried a SUPERSEDED banner, which is an ADR mechanism: ADRs are immutable and get superseded, specs get edited (CONTRIBUTING.md). Marked rather than corrected, it left the contract with four records - this file, the remote-sync design, a spec under ref/ that says not to cite it, and a comment in remote-sync.mjs naming errors.ts the definition because this file was dead.

Rewritten from the implementation. The endpoint list was eight, of which POST /v1/pairing/exchange and POST /v1/credentials/<id>/revoke do not exist and four that do were absent; it is now the ten server/src/app.ts registers. The pairing and per-device-credential section described a removed model and is replaced by the four registration and lookup endpoints, taken from connectSchema, resolveTeamsByName and getTeamSnapshot.

Authorization: Bearer was required on everything but health; the server's only mention of that header is redacting it from logs. The reason it carries no credential, and the two consequences that rest on it - a repeat connect writing nothing, and the network being the trust boundary - are stated where a reader meets them.

The error table listed six codes for the removed model and omitted three the server emits. It is now the set derived from every ProtocolError construction, and says what it does not cover rather than leaving 502/503/504 looking protocol-defined.
ref/README.md said nobody builds toward anything under it and that no document there may be cited. Eleven documents lived there; nine describe shipped code and were cited 34 times from outside, including by scripts/key.sh, scripts/remote-sync.sh, sqlite-sync.sh and server/spec/v1.md. Making the distinction visible from the path did not stop anyone citing them, so the directory goes rather than the warning getting louder.

Promoted nine: the Stage-1 contract, the age-v1 profile, read-state, retention-gap and opaque-envelope specs, ADRs 0005-0007, and the adaptive-catchup design that remote-sync.mjs names in its own comments. Deleted two: device-pairing, which has no implementation and no subcommand, and remote-sync-dogfood, which asked to be deleted once integration/remote reached main (17d0ba7) and taught a connect --token-stdin that no longer parses.

Promotion is not a move. Statuses said proposed or dogfood, which was the vocabulary of the directory they sat in; the specs are current and the ADRs accepted, edited first where they still described the pairing and per-device-credential model the server no longer has. Editing them is what the ADR rule allows while they are proposed and unadopted, and leaving that text under docs/adr/ would have shipped a wrong record rather than an unfinished one.

The last promotion out of ref/ (75b8b1c, ADR 0003) left five relative links pointing one directory too deep and nothing noticed. All 38 links this move invalidated were rewritten by resolving each basename against the tree, and the checker added in the first commit is what enumerated them. Prose and code paths are a separate instrument: 28 more references live in security.md, security.ja.md and three script headers, where no link checker can see them.
The rewrite fixed the href and left the label reading ../spec/age-v1-profile.md, a path that no longer exists. A link checker reads the target and never the text, so this is the shape it cannot see; swept the tree for label/href basename disagreement and this was the only one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant