Skip to content

ci: check:adr-anchors — governed code must keep naming its decision (#3723 follow-up) - #4575

Merged
os-zhuang merged 1 commit into
mainfrom
claude/sys-member-role-assessment-s09jvx
Aug 2, 2026
Merged

ci: check:adr-anchors — governed code must keep naming its decision (#3723 follow-up)#4575
os-zhuang merged 1 commit into
mainfrom
claude/sys-member-role-assessment-s09jvx

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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:

  • ADR-0057 D4 — feed app names to better-auth "only so invitations are accepted — never as the authority for RBAC"
  • ADR-0090 D3 — the word ban: "capability = permission_set · distribution = position … The word 'role' does not exist here"
  • ADR-0095 D3"no enforcement-time code path may consult the better-auth role directly"

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.ts cited ADR-0105 D8 (why delegated_admin is 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.mjs verifies each anchored file exists, each listed ADR has a real record under docs/adr/, and each id still appears in the file. Wired into Lint & Type Check next to the existing check:role-word ratchet.

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

An accepted ADR binds until a superseding ADR says otherwise. Reversing a recorded decision is itself a decision: it needs a new ADR (or an amended status line on the old one), not a changeset that quietly does the opposite. […] when you implement an ADR's decision, leave its id in the code […] A decision nobody can find is a decision that will be reversed.

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:

Case Result
Strip ADR-0108 from auth-manager.ts ✅ fails, printing the invariant
Anchor a non-existent file ✅ fails
Anchor withdrawn ADR-0107 (no record) ✅ fails
Malformed id ("nonsense") ✅ fails
Seed as-is OK (8 anchored file(s))

check:role-word, check:doc-authoring and eslint on the new script all clean. Releases nothing — the changeset has empty frontmatter (no package changes).

Reviewer notes

  • This is a governance change — Prime Directive [WIP] Add Chinese version of the documentation #13 binds every agent in the repo. If you'd rather it stayed advisory, dropping the AGENTS.md hunk leaves the script fully functional.
  • The check runs on the tree, not the PR diff, so it needs no CI context and works locally (pnpm check:adr-anchors).
  • It does not replace per-invariant tests; each anchored decision keeps its own enforcement (e.g. membership-role-vocabulary.dogfood.test.ts for ADR-0108).

🤖 Generated with Claude Code

https://claude.ai/code/session_0186LhwkUBupmLJUUAMda5hU


Generated by Claude Code

…#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
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 2, 2026 8:18am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd dependencies Pull requests that update a dependency file tooling size/m labels Aug 2, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 09:02
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit 21d29cf Aug 2, 2026
20 checks passed
@os-zhuang
os-zhuang deleted the claude/sys-member-role-assessment-s09jvx branch August 2, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants