One record per contract: dissolve ref/, and make server/spec/v1.md true - #863
Open
fujibee wants to merge 11 commits into
Open
One record per contract: dissolve ref/, and make server/spec/v1.md true#863fujibee wants to merge 11 commits into
fujibee wants to merge 11 commits into
Conversation
fujibee
force-pushed
the
docs/one-record-per-contract
branch
2 times, most recently
from
August 18, 2026 20:40
a6defb3 to
a568a06
Compare
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.
…om it It named the drivers, the tools they shell out to, the releases each was phased into, and the issue numbers implementing them. None of that is the decision, and all of it dates: one phase never shipped, so the document described a plan rather than a boundary. Kept: the locked storage_* ABI, the use-case contract with its opaque driver-issued cursor, and the scope boundary that leaves registry and run-state outside the storage axis. The third decision was written around one specific backend; it now states the boundary itself, which is what would survive that backend being swapped. Removed a consequences paragraph tracking a live call-site gap. That belongs in an issue, not in a decision record, and it is reported separately rather than deleted quietly. Same rule applied to what this branch added elsewhere: the Stage-1 spec no longer names which bundled driver advertises which capability, only that capabilities are advertised separately, and ADR 0005 hands the envelope key field name back to the protocol spec that defines it.
Stage 1, Stage 2 and Stage 3 were a delivery order, not a distinction a reader needs. They ran through the normative specs and one of the filenames, so a future reader had to learn a three-step plan - one step of which was never built - before learning what the documents cover. The split is messages versus read state, so the documents say that: stage-1-remote-sync.md becomes message-synchronization.md, pairing with the read-state-synchronization.md beside it, and both titles now name their subject. Stage 3 was server-sent events and wake delivery; that section now says what is out of scope without implying a numbered step someone is waiting on. The capability strings stage1-sync, stage1-resync and stage2-read-state are NOT renamed. Drivers already advertise them and an external driver may too, so changing them breaks the driver ABI rather than a document. They are explained once, where the capabilities are defined, as fixed names whose numbers carry no meaning beyond telling the three apart.
The v1 error contract contradicted itself 760 lines apart: the common-envelope paragraph still assigned binding ids to 401, 429 and a team-scoped 503, and told servers to send Retry-After on 429, while the derived table said the reference server emits none of them. The paragraph now lists only the statuses that exist, and Retry-After is named as something an intermediary sends rather than a protocol response. Two documents promoted in this branch still described the model it removes. The remote-sync design was marked not implemented yet although the HTTP contract now sends readers there for the rationale, and it named two deleted documents as if they were on disk. The message-synchronization spec still gave the HTTP engine authentication to own and authentication material to hold, which the contract it is consumed through says does not exist. Nothing executable changes.
The checker matched inline links only. The tree uses the other form too - docs/spec/age-v1-profile.md links its age reference as [age v1 file][age-format] with the definition further down - so breaking that definition, or deleting it, left the check green. A guard whose whole purpose is naming broken pointers could not see a form the documents actually use. Extraction moves to tests/helpers/check_doc_links.py so it can run against a directory as well as the repository, and now covers three forms: inline targets, reference definitions with a relative target, and reference uses whose label must be defined. Fenced blocks and inline code spans are stripped first, because a backticked character class reads as a reference link to any matcher that skips that step. The new case asserts against a fixture rather than the repository, so it keeps testing the parser on a day when no document happens to be broken. Verified both directions on the real file as well: repointing the age-format definition at a missing path, and deleting the definition, each turn the check red. The reported counts now say what they count. Definitions were previously tallied after the external-URL skip, so a tree holding 31 of them reported 0 - a number that reads as absence of the form rather than absence of work.
The added cases used bare [[ ]] in non-last positions, which cannot fail a test on the macOS bash the suite also runs under. The repository already guards this: the enforceable-assertions count went from its 638 baseline to 642, and two tests failed on the count itself. Each is now chained with || so it is enforced on both shells, and reports which pointer form went unseen rather than failing anonymously. Chaining has to stay on one line - a line continuation before the || hides the chain from the checker and the count went to 643 on the first attempt. Verified by blinding the checker to reference definitions again: the case goes red and names the form it missed. Count is back at 638 exactly, with no line from this suite in the report.
It was marked accepted on one identifier's occurrence count. The document also decides a single-owner boundary the server has no model for, a pending_remote_acceptance gate that appears nowhere in the tree, and a roster-mutation protocol whose storage columns exist but which has no endpoint to run over. Counting member_id established the part that shipped, not the decision. Same standard as 0003: it stays proposed until a reader can check the whole of it, and the status line now names the three parts that are not there rather than leaving the next reader to find out. 0005 was re-measured against the same standard and holds: every one of its five decisions has an implementation to point at after the edits in this branch.
The rule said status becomes accepted when merged, which collapses two different events. main already holds ADRs that are merged and still proposed, so the written rule did not describe the repository, and its unconditional immutability clause then read as forbidding any correction to them. A review stopped on exactly that this week. Acceptance is now its own moment: a record stays proposed until the decision is one a reader can check the tree against, and sitting on main as proposed is named as a normal state. A proposed ADR may be edited in place. Immutability starts at acceptance and covers what was decided - typos, broken links and formatting stay correctable, while changing a decision goes through a superseding ADR as before. This is the ordinary lifecycle those records already follow elsewhere; the previous wording was the outlier. Both language versions carry it, and a test now pairs them: the rules lived in two files with nothing comparing them, so correcting one would have left the other telling contributors the opposite.
fujibee
force-pushed
the
docs/one-record-per-contract
branch
from
August 19, 2026 21:22
cda0846 to
c58c483
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Affects existing users only as documentation. No behaviour change: every code edit in this branch is a comment. Nothing that runs changes, and the shipped driver capability strings are deliberately untouched (see below). Everything else is
docs/andserver/spec/. This is the "something broken is repaired" kind — a reader following these documents was being sent to a removed model, a dead path, or a numbered plan that no longer means anything.What was wrong
One contract had four records, and the one with the authoritative filename was the dead one.
server/spec/v1.mddocs/design/remote-sync.mddocs/spec/ref/stage-1-remote-sync.mdref/, whose README says do not cite one as the reason something is the way it isscripts/internal/remote-sync.mjs:1472errorBody()… is the definition, sinceserver/spec/v1.mdis marked SUPERSEDED"supersededis an ADR mechanism.CONTRIBUTING.md:52states 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.mdRewritten from the implementation rather than patched paragraph by paragraph.
POST /v1/pairing/exchangeandPOST /v1/credentials/<id>/revokedo not exist anywhere inserver/src; four that the server does register were absent. It now lists the tenserver/src/app.tsregisters. Cross-checked by extracting both sets and diffing: 0 difference.Authorization: Beareron 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 repeatPOST /v1/connectwriting nothing, and the requirement not to expose a server to a network whose reachability you would not treat as authorization.connectSchema,connectTeam,resolveTeamsByNameandgetTeamSnapshot— including409 team-already-existsand why a credential-free route depends on it writing nothing at all.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 everyProtocolErrorconstruction, and says explicitly what it does not cover, so502/503/504stop looking protocol-defined.ref/is dissolvedref/README.md— one blob copied into three directories — said "Nobody is building toward anything in aref/directory" and "Do not implement from a document underref/. 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 fromscripts/key.sh,scripts/remote-sync.sh,scripts/drivers/storage/sqlite-sync.shandserver/spec/v1.md. Making the distinction visible from the path did not stop anyone citing them.Promoted nine, each measured against the implementation first:
docs/spec/stage-1-remote-sync.mdstorage_sync_*operations implemented;stage1-resyncadvertised by sqlite and not by jsonl, exactly as the spec's "optional capability" saysdocs/spec/age-v1-profile.mdage-v1102 occurrences,X255198; its conformance vectors already sat outsideref/docs/spec/read-state-synchronization.mddocs/spec/retention-gap-resynchronization.mdresync-requiredinremote-sync.mjs,storage.ts,errors.ts, plus three test filesdocs/spec/server-opaque-envelope.mdkey_id/cipher/blobinprotocol.ts,storage.ts,001_initial.sqldocs/adr/0005-remote-sync-contract.mddocs/adr/0006-composite-read-state-frontier.mdfrontier45 occurrencesdocs/adr/0007-stable-member-and-roster-identity.mdmember_id321 occurrencesdocs/design/adaptive-sync-catchup-v1.mdremote-sync.mjs:2876-3135, which cites this document by name in three comments; 23 lines of testsDeleted two.
device-pairing.md(811 lines):pair-v1andSASappear nowhere in the tree andkey.shhas norequest/approvesubcommand.remote-sync-dogfood.md(177 lines): it asked to be deleted onceintegration/remotereachedmain, which17d0ba7did, and its runbook teachesconnect --token-stdin, a flagremote.shno longer parses — one comment is all that survives of it.Promotion is not a move
Statuses said
proposedordogfood— the vocabulary of the directory they sat in. Left alone, the documents would have been half-promoted: authoritative location, provisional label. The specs are nowcurrentand the ADRsaccepted.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:
manifest-terminal-acknowledgement mechanism, which has no implementation. Its "key epoch" is written askey_id, the name the protocol actually uses.Editing them is what the ADR rule permits while a document is
proposedand unadopted (AGENTS.md: squash while single-owner and unmerged; the supersede chain becomes mandatory only aftermain). Flagging it anyway, because it is the judgement call in this PR — the alternative was to shipdocs/adr/containing paragraphs I had measured to be describing a removed mechanism.stage-1-remote-sync.mdalso 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.jsonhas 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:
docs/adr/0003, which75b8b1cmoved out ofdocs/adr/ref/without adjusting its relative depth — the last promotion out ofref/broke five links and nothing noticed;Committing it red first is the positive control: the failure is in the branch history where it can be read, rather than reconstructed later from a description. A checker introduced alongside the fix is green from birth and no one has seen it fail.
mainnever receives it red — the later commits close every pointer it names.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.
ADR 0003 keeps its decisions and loses its implementation
It named the drivers, the tools they shell out to, the releases each was phased into, and the issue numbers implementing them. None of that is the decision, and all of it dated: one phase never shipped, so the document described a plan rather than a boundary. Kept: the locked
storage_*ABI, the use-case contract with its opaque driver-issued cursor, and the scope boundary that leaves the team registry and run-state outside the storage axis. The third decision had been written around one specific backend; it now states the boundary itself, which is what survives that backend being swapped.Measured after the rewrite: driver and tool names 0, release numbers 0, issue numbers 0. The three remaining "JSONL" mentions are the interchange format, not the driver.
Status stays
proposed, deliberately. The decision it records — that a storage backend is replaceable — has not been demonstrated: one driver ships, and a second one works only because core tests for it by name. Marking it accepted would assert something the tree does not yet show.A consequences paragraph tracking a live call-site gap was removed. A decision record is not where live work is tracked, so it is filed as its own issue rather than deleted quietly.
Stage 1, 2 and 3 are gone from the prose
They were a delivery order, not a distinction a reader needs, and they ran through the normative specs and one of the filenames. A future reader had to learn a three-step plan — one step of which was never built — before learning what the documents cover.
The real split is messages versus read state, so the documents say that.
docs/spec/stage-1-remote-sync.mdbecomesdocs/spec/message-synchronization.md, pairing with theread-state-synchronization.mdbeside it, and both titles now name their subject. "Stage 3" was server-sent events and wake delivery; that section now states what is out of scope without implying a numbered step someone is waiting on.The capability strings
stage1-sync,stage1-resyncandstage2-read-stateare NOT renamed. Drivers advertise them today and an external driver may too, so changing them breaks the driver ABI rather than a document. They are explained once, where the capabilities are defined, as fixed names whose numbers carry no meaning beyond telling the three apart. Verified untouched: bothcapabilities=lines and all three engine-side checks are unchanged.Stage prose remaining in
docs/andserver/spec/: 0, counted with the capability identifiers excluded so the two are not confused.Verification
ref/directoriesref/pathref/references, path and name instrumentsv1.mdvs registered inapp.tsThe 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 code —
docs/security.mdanddocs/security.ja.mdcite 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_contractin CI'sxargs batsform.docs/spec/vectors/verify-age-v1-vectors.mjsexits 0.test_remote_sync_enginepasses. Local form is not CI's shard form.Destination drift re-measured at commit time:
origin/mainisf7de22aand the merge base isf7de22a.CI on the current head: 22 checks, 22 completed, no failures.
Review findings addressed in this branch
A static review found three contract conflicts the rewrite had left behind, and one claim in this body that did not match the tree. All four are fixed above; they are listed because the body previously asserted a cleanliness it did not have.
401,429and a team-scoped503, and instructed servers to sendRetry-Afteron429, while the derived table said the reference server emits none of them. The paragraph now lists only statuses that exist, andRetry-Afteris attributed to an intermediary.docs/design/remote-sync.mdwas marked "Not implemented yet" although the HTTP contract now sends readers there for the rationale, and it named two deleted documents as if they were still on disk.docs/spec/message-synchronization.md, promoted tocurrent, still gave the HTTP engine authentication to own and authentication material to hold — the removed credential model surviving in a document this branch promotes.docs/design/remote-sync.mdnamed the deleteddevice-pairing.md, so the name instrument was not at 0. It is at 0 now, re-measured after that file was fixed, and the verification row says so rather than quietly reading 0.Two findings are not addressed here and are held for a ruling. The rule permitting an unadopted ADR to be edited is bounded by whether the document reached
main, not by its status — and both ADRs this branch edits already exist onmainunderref/, soCONTRIBUTING.mdwould require superseding ADRs rather than edits. Separately, one of them is markedacceptedon evidence covering a single clause:pending_remote_acceptancedoes not exist in the tree, there is no server-side owner model, and no roster-mutation endpoint exists for the mutation-dedupe-then-CAS contract, although its storage columns do. Both are questions about ADR discipline rather than about this rewrite, and neither is decided in this PR.Scope
Only the records named above and the guard that keeps their pointers honest.
key rotatedocumentation, device pairing, and the roster-mutation protocol are not addressed here.After review
Four findings from static review are closed in this branch, and one was withdrawn.
](only, while the tree links its age reference as[age v1 file][age-format]with the definition further down — so breaking or deleting that definition left the check green. Extraction moved totests/helpers/check_doc_links.pyand now covers inline targets, reference definitions with a relative target, and reference uses whose label must be defined; fenced blocks and inline code spans are stripped first, because a backticked character class otherwise reads as a reference link. A case asserts this against a fixture rather than the repository, so it keeps testing the parser on a day when nothing in the tree happens to be broken. Verified both ways on the real file: repointing the definition at a missing path, and deleting it, each turn the check red.proposed. It had been marked accepted on one identifier's occurrence count. The document also decides a single-owner boundary the server has no model for, apending_remote_acceptancegate that appears nowhere, and a roster-mutation protocol whose storage columns exist but which has no endpoint to run over. Its status line now names the three parts that are not there. ADR 0005 was re-measured against the same standard and holds.CONTRIBUTING.mdnow records that publicly so the next reader does not stop where this review did — in both language versions, with a test pairing them, since the rules lived in two files and nothing compared them.The new assertions are chained so they can fail on the bash the suite also runs under; the repository's own enforceable-assertions count is back at its baseline exactly, with no line from this suite in the report.