Commit 825d70f
docs(identity): re-point the SCIM/identity ADR-0071 citations at the records that mean them (#18098)
Part of #14361 — ⛔ this PR deliberately does NOT close it; see *"What
this PR leaves open on the same card"* below.
Clause-②: no
Director ruling **B — the repo-qualified spelling** (comment
`5507409725`), **as amended by the same director seat in comment
`5507573601`**, which is the operative form of the ruling and is
independently mandated by AGENTS.md Prime Directive #13. See *"The one
place this PR departs from the dispatch brief"* below — please read it
before reviewing the diff.
## What was wrong
From this repository's point of view `ADR-0071` named two unrelated
decisions and only one of them had a record here.
- `docs/adr/0071-dataset-semantic-layer-depth.md` is **ADR-0071: Dataset
semantic-layer depth — multi-hop joins**.
- The identity / SCIM citations mean something else entirely: the
enterprise-identity decision taken in `objectstack-ai/cloud`, whose
**open mechanism half** has been mirrored into this repo since
2026-09-07 as
[ADR-0134](../blob/main/docs/adr/0134-env-side-scim-provisioning.md).
`check:adr-anchors` was green over all of them, because the number
*resolves* — it just resolves to a real page about the wrong subject,
which is worse than a dangling id: a plausible record invites belief
instead of a second question.
## The count, re-derived from the tree
The card said 39, measured on `e854a531a`. Re-derived on this branch's
merge base `66e34d14d`, per line rather than per file:
| bucket | lines | disposition |
|---|---|---|
| identity meaning, bare | **44** | re-pointed (43 → `ADR-0134`, 1 →
`cloud ADR-0071`) |
| identity meaning, already repo-qualified | 1 | untouched —
`auth-plugin.ts:1210` `cloud ADR-0071 verification #1` |
| dataset meaning | 22 | ⛔ untouched — they match the local record |
| `docs/adr/**` (the 0071 record, ADR-0134, ADR-0135) | 18 | ⛔ untouched
— governed surface |
| six package CHANGELOGs | 15 | ⛔ untouched — historical archive |
| generated `content/docs/references/system/auth-config.mdx` | 2 |
regenerated from its producer, never hand-edited |
| **total `ADR-0071` lines in tracked files** (`pnpm-lock.yaml`
excluded) | **102** | over 46 files |
**How 44 differs from 39.** The card counted *files* (43, of which 39
identity); this counts *citation lines*, and several files carry more
than one (`auth-manager.ts` 8, `auth-manager.test.ts` 9,
`auth-config.zod.ts` 4). The surface also grew between `e854a531a` and
today: the two mirror ADRs landed, `auth-manager.test.ts` and
`last-admin-guard.{ts,test.ts}` gained citations, and the two changesets
the card counted have since been released into CHANGELOGs (`.changeset/`
carries no `ADR-0071` today). 24 source files are touched; the 39 in the
card is neither the file count nor the line count of what actually
needed moving.
## How each citation was classified — per site, by meaning, never by
path
The split is not a judgement call this PR invented. **AGENTS.md Prime
Directive #13** states it directly:
> Decisions that draw the open/closed or commercial boundary live in
`objectstack-ai/cloud` and are cited from this repo as `cloud ADR-NNNN`
— ⛔ never as a bare number […] When a cloud decision's **mechanism
half** governs open code here, this repo carries its own ADR — own
number […] the commercial half left in cloud.
So the question asked at every site was: *which half of the cloud
decision is this sentence about?*
**(b) the open mechanism half → `ADR-0134`, 43 sites.** Every one of
them is describing code that lives in this repository, and ADR-0134's
own Consumers list names those exact files:
- `plugin-auth` (14) — effective SCIM forces the better-auth `admin`
plugin on; the construction-time refusal when `plugins.admin: false`
sits beside effective SCIM; `active:false` → ban + session revocation;
`@better-auth/scim` accepting no `schema` option; the SCIM/SSO adapter
model map.
- `plugin-auth` tests (10) — the four pins on the refusal message,
re-judged in place (see below).
- `spec` (7) — the `admin` flag's docblock and `.describe()` text,
`public-auth-features.ts`'s `notes`, and the v17 `default-changes.ts`
upgrade note.
- `platform-objects` (9) — the `protection.reason` on the eight
`sys_scim_*` objects and the `sys_user` action note.
- `qa/dogfood` (3), `pnpm-workspace.yaml` (1, the rc.2 seven-model
migration), `docs/qa/platform-checklist/areas/identity-auth.json` (1).
**(a) the commercial / boundary half → `cloud ADR-0071`, 1 site.**
`auth-manager.ts:3682`, *"the paid Identity lifecycle"* — that is D6 of
the cloud record, the paid-Enterprise-Identity framing which ADR-0134
§*What stays in the cloud record* explicitly refuses to restate. It
stays cited to cloud.
**Dataset meaning → untouched, 22 sites.** Each was read, not inferred:
all 22 are about multi-hop `include` chains, the 3-hop limit, join
allowlists and Cube joins — `service-analytics` (15),
`spec/src/ui/dataset.zod.ts` (3),
`lint/validate-dataset-references.{ts,test.ts}` (2), `analytics.mdx`
(1), `query-syntax.mdx` (1).
## Positive control against over-rewriting
The discrimination rule is not a regex over the id — it is an explicit
per-file allowlist of identity files, with a qualifier-aware
substitution inside them. Two independent proofs that nothing on the
dataset side moved:
```
$ git diff --stat origin/main -- packages/services/service-analytics packages/lint \
packages/spec/src/ui/dataset.zod.ts content/docs/data-modeling/analytics.mdx \
content/docs/protocol/objectql/query-syntax.mdx docs/adr/
(empty)
```
and, in the other direction, the already-qualified `cloud ADR-0071` at
`auth-plugin.ts:1210` survived a pass over its own file untouched — the
substitution skips any id already carrying a `CROSS_REPO_QUALIFIERS`
word.
## Bare `ADR-0071` still resolves — the gate is not weakened
Identical summary lines, before and after:
```
main check-adr-anchors: OK (53 anchored file(s) … 35477 citation(s) across 4521 file(s) resolve; 1022 decision-letter citation(s) …)
HEAD check-adr-anchors: OK (53 anchored file(s) … 35477 citation(s) across 4521 file(s) resolve; 1022 decision-letter citation(s) …)
```
22 bare `ADR-0071` citations remain in the tree and the gate resolves
every one of them. **Ablation**, to show that is a measurement and not a
vacuous pass — one bare dataset citation mutated to an id with no
record:
```
on-disk proof : ADR-0071 3 -> 2 ; ADR-9071 injected = 1
ABLATION check-adr-anchors exit = 1
• ADR-9071 is cited by 1 file(s) but names no record under docs/adr/ —
restored blob : 5c90e77
RESTORE SETTLED: blob matches HEAD and 'git diff HEAD' is empty
```
The restore was settled by blob hash plus an empty `git diff HEAD`, ⛔
never by an exit code — `trap` is unreliable in this container (#17875).
## Pins re-judged in place, ⛔ none deleted
`auth-manager.test.ts` carries four `toThrow(/…ADR-0071…/)` pins on the
operator-facing refusal message and one test name. They pin the
message's *content*, and the content moved, so the pins move with it —
the assertions still pin exactly what they pinned before: that the
refusal names the ADR that explains the coupling. Reverse-verified that
they are live rather than decorative: with the shipped message mutated
back to `ADR-0071`, the two message pins fail loudly.
```
MUTATED vitest exit = 1
AssertionError: expected [Function] to throw error matching /plugins\.admin[\s\S]*ADR-0134[\s\S]*p…/
AssertionError: expected [Function] to throw error matching /OS_SCIM_ENABLED[\s\S]*ADR-0134/
restored : f9b5c2a
RESTORE SETTLED: blob == HEAD and 'git diff HEAD' empty
```
## Generated docs
`content/docs/references/system/auth-config.mdx` lines 105 and 215
follow their producer (`auth-config.zod.ts`'s `.describe()`),
regenerated with `pnpm --filter @objectstack/spec gen:schema && …
gen:docs`. Exactly two lines drifted; nothing else in the 222 generated
files moved. `check:docs`, `check:generated` and
`check:authorable-surface` are green.
## Changeset — measured, not defaulted
`patch` for `@objectstack/plugin-auth`, `@objectstack/platform-objects`,
`@objectstack/spec`. Published bytes really do move, measured against
each package's `files[]` after a build:
- `@objectstack/spec` — `files[]` lists `src/**/*.zod.ts`, so the
changed `.describe()` ships verbatim; the generated `json-schema/`
bundle (also in `files[]`) carries it too.
- `@objectstack/plugin-auth` — `dist/index.mjs` carries 4 `ADR-0134`,
including the operator-facing refusal string (positive control: a
shipped literal greps at 1; negative control: a comment-only marker
greps at 0).
- `@objectstack/platform-objects` — `dist/index.mjs` carries the 9
`protection.reason` strings.
`patch` and not `minor`: no export, no schema shape, and no
accept/refuse face moves. The refusal fires on exactly the condition it
fired on before; only the ADR number inside its sentence changes. The
changeset says so, because a deployment grepping that message for
`ADR-0071` is the one consumer this can surprise.
## Reverse-read — which existing sentence does this make false?
Three, all of them in `docs/adr/**`, which this lane ⛔ must not touch
(governed surface, PD #14). None is falsified in substance; each goes
tense-stale:
1. `docs/adr/0134-env-side-scim-provisioning.md:36` — *"Identity code
that writes a bare `ADR-0071` today therefore cites, by this repo's own
convention, the wrong document."* After this lands, no identity code
does. The sentence's headline claim — that `ADR-0071` is an ambiguous
string in this repo — stays **true**: the local 0071 is still the
dataset record and cloud's 0071 still exists.
2. `docs/adr/0134:38` and `:294` — *"Re-pointing the existing bare
citations is #14361's work and is deliberately ⛔ not done by this
file."* Still true about the file; the pointer becomes past tense.
3. `docs/adr/0135-identity-and-access-architecture.md:59` —
*"Re-pointing the identity surface's existing bare citations at this
record is [#14361]"* — same class, and see the scope note below, because
the `ADR-0024` half of that sentence is **still outstanding and still
true**.
**Reverse direction — a sentence this makes true rather than false:**
`docs/adr/0134:37`, *"**Always write `cloud ADR-0071` for the SCIM
record**, and `ADR-0134` for this one."* That instruction was correct
and simply unobeyed by the tree; this PR is the tree obeying it.
**Zero** other sentences in the tree assert a present-tense count or
claim about these citations — the only `39 files` strings in the repo
belong to `packages/cli` and are about something else entirely.
## The one place this PR departs from the dispatch brief
The dispatch brief asks for **every** identity citation to read `cloud
ADR-0071`, citing ruling `5507409725`. Three sources on `main` say
otherwise, and they agree with each other:
1. **The ruling's own amendment**, comment `5507573601`, same director
seat, 14 minutes later, self-titled *"Ruling amended"*: *"**Target for
the (b) set** (citations that mean the cloud decision's *open mechanism
half*): the **new local ADR numbers** that #14506 / #14507 / #14508 land
— not `cloud ADR-NNNN`"*, and *"the operator-facing refusal text in
`auth-manager.ts:359` re-pointed to the new local number"*.
2. **The release comment** `5594577301` that unblocked this card
restates it: *"the (b) citation targets are the new local ADR numbers
those cards land"*, naming ADR-0134 as the mirror of cloud 0071.
3. **AGENTS.md Prime Directive #13**, quoted above, which is binding
regardless of any comment and says mechanism-half → this repo's own
number.
Under the unamended reading, ruling B is still satisfied by this diff
(every identity citation is now unambiguous and the gate discriminates),
but 43 of the 44 would read `cloud ADR-0071` instead of `ADR-0134`. If
the reviewer prefers that reading it is a one-command flip on this
branch; I did not pick a side silently, which is why this section
exists.
## What this PR leaves open on the same card
The amendment also folds the sibling collision class into this card —
bare `ADR-0024` and bare `ADR-0081`, the mirrors of which landed as
ADR-0135 and ADR-0133 — *"one card over the identity surface (0071 +
0024 + 0081) … so it is not paid twice"*. The dispatch brief scopes this
lane to `ADR-0071` only, and the sibling surface is large: **151** bare
`ADR-0024` lines and **86** bare `ADR-0081` lines, most of which
legitimately mean this repo's own `0024-mcp-connectors` and
`0081-trusted-react-page-tier` records and must **not** move.
`docs/adr/0135:260` says so in as many words — *"Whether any individual
bare `ADR-0024` citation should move … cannot be a search-and-replace"*.
That is a real per-site pass with its own budget, and
`docs/adr/0135:260` says so in as many words. `objectql-adapter.ts:58`
now reads `See ADR-0024 / ADR-0134.`, where the `ADR-0024` half is
knowingly left for it.
⭐ **This PR therefore delivers the `ADR-0071` third of #14361 — the 44
sites above — and nothing else.** The per-site judgement over bare
`ADR-0024` and bare `ADR-0081` is the remaining work of **the same
card**, not a new one: the amendment's Scope line reads *"the identity
surface's **whole collision class, one pass**: bare `ADR-0071`, bare
`ADR-0024`, bare `ADR-0081` — every citation read for its meaning"*, and
`docs/adr/0135:260` calls the `ADR-0024` half *"#14361's per-site
call"*. That is why the first line of this body says `Part of #14361`
and not `Closes`: merging this while two thirds of the ruled scope is
unwritten would close the card on a third of its work. The card stays
open for the next round.
## Verification
| what | result |
|---|---|
| `pnpm check:adr-anchors` | green, byte-identical summary to `main` |
| `pnpm --filter @objectstack/{spec,plugin-auth,platform-objects} test`
| 476 + 111 + 40 files, **16 517** tests, all pass |
| same three, `typecheck` | green (incl. `check:test-typecheck` ledgers,
unmoved) |
| `check:authorable-surface` · `check:docs` · `check:generated` | green
|
| `check:platform-checklist` · `check:doc-anchors` ·
`check:docs-single-h1` · `check:doc-authoring` ·
`check:docs-spec-enumerations` | green |
| `check:nul-bytes` · `check:published-files` ·
`check:corpus-claim-drift` · `check:quick-reference-counts` ·
`check:comment-mask-{adoption,corpus}` ·
`check:spec-docblock-symbol-anchors` | green |
| changeset gates (`check-changeset-fixed`, `check-changeset-no-major`,
`check-empty-changeset`, `check:changeset-gate-self-tests`) | green |
| `check:doc-frontmatter` · `check:docs-section-name` ·
`check:keyed-text-bounds` · `check:platform-object-tenancy-census` ·
`check:reference-carrier-shape` · `check:adr-0087-registration` | green
|
| `eslint . --no-inline-config` — the **whole repo**, not a narrowing |
**6743** files, 0 errors, 0 warnings, at `8d2dfb3` |
39 gate commands, every one `exit 0`, captured before any pipe.
**Declared narrowing.** `node scripts/pm/dispatch-gates.mjs --ran`
derives 123 commands from this change set and accounts 39 of them. I ran
the families this card names plus every one I could see implicated, and
— since it turned out to fit the foreground budget — the repo-wide lint
union rather than a narrowing of it. The remaining derived families are
CI's farm, not this lane's run. `packages/qa/dogfood`'s three edits are
comment-only — no dogfood boot was run locally, and that layer is
declared to CI.
---
_Generated by [Claude Code](https://claude.ai/code)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7799fef commit 825d70f
26 files changed
Lines changed: 94 additions & 46 deletions
File tree
- .changeset
- content/docs/references/system
- docs/qa/platform-checklist/areas
- packages
- platform-objects/src/identity
- plugins/plugin-auth/src
- qa/dogfood/test
- spec
- scripts/lib
- src
- kernel
- system
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | | - | |
| 105 | + | |
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
| |||
212 | 212 | | |
213 | 213 | | |
214 | 214 | | |
215 | | - | |
| 215 | + | |
216 | 216 | | |
217 | 217 | | |
218 | 218 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
478 | 478 | | |
479 | 479 | | |
480 | 480 | | |
481 | | - | |
| 481 | + | |
482 | 482 | | |
483 | 483 | | |
484 | 484 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
| 33 | + | |
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| |||
Lines changed: 2 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | | - | |
| 50 | + | |
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | | - | |
| 29 | + | |
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | | - | |
| 29 | + | |
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| |||
0 commit comments