ci: check:adr-anchors — governed code must keep naming its decision (#3723 follow-up) - #4575
Merged
Merged
Conversation
…#3723 follow-up) An accepted ADR is only binding if the code it binds points back at it. #3723 is the proof. Three accepted ADRs said the same thing — ADR-0057 D4 ("never as the authority for RBAC"), ADR-0090 D3's word ban ("distribution = position"), ADR-0095 D3 ("no enforcement-time code path may consult the better-auth role") — and a patch-level changeset reversed all three by making app-declared names storable in `sys_member.role`. A follow-up then made the derivation automatic in every host. The reversal held for a day; the tracking issue was closed, reopened and rewritten three times while the cause moved. The mechanism was not carelessness. `auth-manager.ts` cited ADR-0105 D8 (why `delegated_admin` is registered) and said nothing about why the app-role loop beside it was a violation. Reading that file, an author — human or agent — could not have known. In a repo worked by many agents in parallel, nobody greps the ADR corpus before an edit; the constraint has to be where the edit is. So: a curated map of file → governing ADR ids + the invariant, and a check that the ids still appear in the file. Deliberately dumb — it is a presence check, not a proof of obedience (no static check can be that). Its value is the failure text, which states the INVARIANT rather than "put this string back", and that it fires on exactly the diff worth a second look: someone rewriting a governed block and dropping the rationale with it. Seeded with 8 anchors, all in the incident's blast radius: the membership-role cluster (spec, auth-manager, auth-plugin, both platform objects), the authz resolver, the delegated-admin gate, the invitation role cap. Deliberately not more — a map of everything is a map of nothing, and each entry must earn its failure mode. Also AGENTS.md Prime Directive #13: reversing a recorded decision is itself a decision and needs a superseding ADR, not a changeset that quietly does the opposite; and when you implement an ADR, leave its id in the code. Verified by temporarily stripping `ADR-0108` from auth-manager.ts (fails, with the invariant), plus the missing-file, withdrawn-ADR (0107) and malformed-id paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186LhwkUBupmLJUUAMda5hU
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
os-zhuang
marked this pull request as ready for review
August 2, 2026 09:02
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.
Follow-up to #3802 (ADR-0108). This is the recurrence guard for the process failure behind #3723, not the vocabulary bug it already fixed.
The failure, stated precisely
Three accepted ADRs said the same thing:
permission_set· distribution =position… The word 'role' does not exist here"A patch-level changeset reversed all three by making app-declared names storable in
sys_member.role. A follow-up then made the derivation automatic in every host. The reversal held for a day, and the tracking issue was closed, reopened and rewritten three times while the cause moved.The mechanism was not carelessness.
auth-manager.tscited ADR-0105 D8 (whydelegated_adminis registered) and said nothing about why the app-role loop directly beside it was a violation. Reading that file, an author — human or agent — could not have known. In a repo worked by many agents in parallel, nobody greps the ADR corpus before an edit. The constraint has to be where the edit is.What this adds
scripts/adr-anchors.json— a curated map of file → governing ADR ids + the invariant:{ "file": "packages/plugins/plugin-auth/src/auth-manager.ts", "adrs": ["ADR-0108"], "invariant": "better-auth's organization roles map registers the closed framework vocabulary ONLY. App-declared `position` / `permission` names are not organization roles — registering one makes it storable in `sys_member.role`, which `resolve-authz-context` projects into `current_user.positions`." }scripts/check-adr-anchors.mjsverifies each anchored file exists, each listed ADR has a real record underdocs/adr/, and each id still appears in the file. Wired intoLint & Type Checknext to the existingcheck:role-wordratchet.Deliberately dumb. It is a presence check, not a proof of obedience — no static check can be that, and pretending otherwise would be its own
declared ≠ enforced. Its value is twofold: the failure states the invariant rather than "put this string back", and it fires on exactly the diff that warrants a second look — someone rewriting a governed block and dropping the rationale with it.Seeded with 8 anchors, all in the incident's blast radius: the membership-role cluster (spec, auth-manager, auth-plugin, both platform objects), the authz resolver (ADR-0095 D3), the delegated-admin gate (ADR-0090 D12), the invitation role cap (ADR-0105 D8). Deliberately not more — a map of everything is a map of nothing, and each entry must earn its failure mode. The script's header documents the test for adding one: if a reasonable engineer could "fix" this code and be reverting a decision, anchor it.
AGENTS.md Prime Directive #13
This is the half the script cannot enforce, and the rule that was actually broken.
Verification
Negative-tested, since a guard that never fails is decoration:
ADR-0108fromauth-manager.tsADR-0107(no record)"nonsense")OK (8 anchored file(s))check:role-word,check:doc-authoringandeslinton the new script all clean. Releases nothing — the changeset has empty frontmatter (no package changes).Reviewer notes
pnpm check:adr-anchors).membership-role-vocabulary.dogfood.test.tsfor ADR-0108).🤖 Generated with Claude Code
https://claude.ai/code/session_0186LhwkUBupmLJUUAMda5hU
Generated by Claude Code