Repository navigation
fix(plugin-security,core): security/explain resolves the user it explains through enforcement's organization-claim check (#20580) - #20614
Conversation
…er through enforcement's organization-claim check Explaining another user in the caller's organization is a claim on that organization made for them. Enforcement vets a session's claim before it resolves anything (the session arm of resolveAuthzContext): under a walled posture a claim no current membership backs is dropped and the user resolves with no active organization. The explainer skipped that check, so a member removed from the caller's organization was explained holding that organization's grants. The check is extracted, with no behaviour change, into one exported core function, vetOrganizationClaim, which the session arm and the explainer both ask. Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude <noreply@anthropic.com>
…al, so Layer 0 stays out of the compared verdict Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude <noreply@anthropic.com>
pin header's measurement provenance, and the changeset Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 34 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 65dec923a50e6edd79aafee6fbeefac00c11cf35 && git checkout 65dec923a50e6edd79aafee6fbeefac00c11cf35
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eb4b17c346c3a82aafe09462bda3b732fa3767bf bd7b4677773987cf5011dbde81e069167a69736e && git checkout -B drift-repro eb4b17c346c3a82aafe09462bda3b732fa3767bf && git merge --no-ff bd7b4677773987cf5011dbde81e069167a69736e
node scripts/docs-audit/affected-docs.mjs --json eb4b17c346c3a82aafe09462bda3b732fa3767bf
|
Contract reviewServed-tier: ① Derived judgmentsInputs read: card #20580 (body and all four comments — the serial notice, the claim with its in-place Clause correction, the Accept-set changes (runtime behaviour):
Public-surface changes:
Scope against the claim Pin quality: Check-runs on the head, as read: success — Build Core, Governed Surface Queue Guard, Check Changeset, Type Check · debt ledger, Type Check · source gates, Dogfood Regression Gate (3/3), Dogfood Verify CLI, Check PR Size, Check Documentation Links, Flag docs affected by code changes, the five PR-shape guards (branch claim, single-writer path, same issue, part-of), filter, Auto Label. in_progress — Test Core (1/6 through 6/6), Dogfood Regression Gate (1/3, 2/3), Temporal Conformance (live PG + MySQL), Type Check · consumer gates, Type Check · workspace, Lint & Repo Gates. skipped — Console Pin Gate (an additive change, nothing for the pinned sibling to lose), Build Docs, Packed-tarball smoke. No failure at the time of reading. The seat checks convergence before landing; this verdict judges ①②③. ② Semver levelChangeset Clause-②: yes (widening) That is the PR body's line as corrected by the seat from ③ Boundary flagsDev deviations (
Open question (finding 1 — the explained context carries no organization of its own): A, as the seat answered ( Out-of-scope findings:
Reviewer findings (this record's own, none verdict-bearing):
Implemented-by: VERDICT: PASS Generated by Claude Code |
…me/src to the commits that decided them (objectstack-ai#20624) Part of objectstack-ai#20594 Clause-②: no ## What changed This is stage 1 of the `domain:cli` lane of the dead-citation sweep: `packages/runtime/src/**`, the lane's largest package. Every comment or docblock site in scope that cited a tracker number answering 404 now cites, in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), the commit in this repository's history that decided what the line describes, and says in its own words what that commit decided. PR objectstack-ai#20533 is the method and PR objectstack-ai#20609 the closest sibling. Later stages cover `rest`, `cli`, `types` and the rest of the lane, so this PR says `Part of` and the card stays open. That is **513 comment sites on 508 lines in 118 files, covering 96 numbers**: 194 of the census's 217 sites, and 319 more in test comments, which the census defers. Three more sites carried a slash-joined dead number the citation grammar does not read (`objectstack-ai#10629/objectstack-ai#10630`, `objectstack-ai#5811/objectstack-ai#12281`, `objectstack-ai#8421/objectstack-ai#12194`), and they are rewritten too. Each rewritten line cites one of **95 distinct commits**. No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/` records the decision behind any of these numbers. ADR-0126 and ADR-0131 name objectstack-ai#10243 only as the incident, ADR-0126 names objectstack-ai#11513 only for the flow-clone half, and ADR-0112 names objectstack-ai#12281 only as another card. So every anchor is a commit. The anchors the landed stages already gave the same numbers are reused (24 numbers, for example `f19475c0a` for objectstack-ai#14143, `e2798fab7` for objectstack-ai#6345 and `79c46da90` for objectstack-ai#9934), so each number carries one anchor across the tree. Only comments changed. Every touched file keeps its line count (508 lines out, 508 in, over 118 files), so no line citation into these files moves. Seven of the 508 lines held no census site. Five are the other half of a sentence that had to change: `action-governance-scope-divergence.test.ts:6` (「the card names」 to 「that diverged」, because line 4 no longer names the card), `action-record-load-denied.test.ts:560`, `dispatcher-5xx-demoted-code-withhold.test.ts:45` (「that card's change」 to 「that commit's change」), `hook-input-writeback-readonly-provenance.integration.test.ts:380` (「that card」 to 「that commit」) and `standalone-stack-seeder-declaration-copy.test.ts:88` (a trailing 「PR」 whose number wrapped onto line 89). Two carry only a slash-joined number: `dispatcher-plugin.ts:688` and `meta-compound-arity-mint-door.test.ts:4`. No code token moves (see the guard below). **No citation number is added.** Every tracker number on an added line was already on the line it replaces. No PR number stands on an added line, and none of the 95 shas is on a removed line. Twenty-eight dead comment sites are left on purpose: - **20 in `domains/meta.ts`.** PR objectstack-ai#20615 (objectstack-ai#20590's) opened at 2026-09-29T08:12:40Z, after this stage's claim and first read, and edits that file. So the file went back to its base blob (`b4ddb362cc`) in `a5cdfd8a46`, as PR objectstack-ai#20612 did with `authoring-rules.ts`. The anchors are verified and listed below for the follow-up. - **8 with no deciding commit, or with a literal reader.** See "The sites left" below. One more file: a `patch` changeset for `@objectstack/runtime`, because the rewritten docblocks ship (see Changeset below). ## Census: `packages/runtime`, before and after **Instrument.** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged, run with the fleet token. Its surface is comment prose in `packages/**/src/**/*.ts` with string literals blanked, and it defers `*.test.ts`. The count is its `allocated-but-absent` findings under `packages/runtime/`. Both runs enumerated the whole board (185 pages), so neither read a truncated board. | reading | tree | board | whole-repo `allocated-but-absent` | runtime sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `eb4b17c346`, run 2026-09-29T07:55:51Z to 08:05:47Z | enumerated, 185 pages, frontier objectstack-ai#20614, 18,441 numbers | 2,397 | **217** | 216 | 29 | 59 | | after | head `a5cdfd8a46`, run 09:08:23Z to 09:14:11Z | enumerated, 185 pages, frontier objectstack-ai#20623, 18,450 numbers | 2,027 | **23** | 23 | 4 | 12 | The before count equals the card's 217 at `f11b5f20a2`. The 23 left are the 20 held `domains/meta.ts` sites and 3 deliberate ones (`api-exposure.ts:108`, `domains/mcp.ts:360`, `route-ledger.ts:300`). The whole-repo drop is 370: this diff's 194, plus the 97 and 79 of PR objectstack-ai#20609 and PR objectstack-ai#20612, which landed on `main` in between and came in with the merge. **Supplementary instrument, the whole scope.** The census does not read test files or strings, and this stage's scope includes test comments. So a second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) and `classifyCitation` over every `.ts` file under `packages/runtime/src` (373 files), against a board probed by REST for every number cited there. The lit controls objectstack-ai#16862, objectstack-ai#16847 and objectstack-ai#17698 answered 200 and the dead controls objectstack-ai#16714, objectstack-ai#16715 and objectstack-ai#16697 answered 404 in both runs. | reading | tree | citations | dead | src comment | test comment | src string | test string | |---|---|---|---|---|---|---|---| | before, 08:17:55Z | `eb4b17c346` | 5,364 | **641** | 217 | 324 | 5 | 95 | | after, 09:24:24Z | `a5cdfd8a46` | 4,851 | **128** | 23 | 5 | 5 | 95 | Its src-comment column equals the census's 217 and 23, which is the control on the second instrument. The 4,499 resolving citations, the 195 that resolve as pull requests and the 29 cross-repo ones are the same in both readings. The drop is 513, exactly this diff's grammar-read sites. ## Per-number table Sites and files are the dead comment sites in scope at the base, tests included. `held` is `domains/meta.ts` (see above) and `left` is a site with no deciding commit or with a literal reader. `strings kept` counts string-literal sites, which are tokens and stay as they were. Every anchor was read in its message or its diff, not only in its subject: it is the commit that made the change the line describes, and its own message or diff names the number it replaces. | number | comment sites / files | rewritten | held | left | strings kept | anchor | |---|---|---|---|---|---|---| | `objectstack-ai#6065` | 1/1 | 1 | 0 | 0 | 0 | `026101660` | | `objectstack-ai#6123` | 1/1 | 1 | 0 | 0 | 0 | `59d1933f9` | | `objectstack-ai#6206` | 5/2 | 5 | 0 | 0 | 0 | `8e13ca876` | | `objectstack-ai#6216` | 2/1 | 2 | 0 | 0 | 1 | `f586f1a89` | | `objectstack-ai#6220` | 1/1 | 1 | 0 | 0 | 0 | `83df2fd73` | | `objectstack-ai#6238` | 2/2 | 2 | 0 | 0 | 2 | `c8d6f6e08` | | `objectstack-ai#6259` | 4/2 | 3 | 0 | 1 | 1 | `6968885ef` | | `objectstack-ai#6265` | 12/2 | 12 | 0 | 0 | 4 | `cfb549db8` | | `objectstack-ai#6268` | 9/3 | 9 | 0 | 0 | 0 | `68f5eccb1` | | `objectstack-ai#6287` | 1/1 | 1 | 0 | 0 | 0 | `84c86fb45` | | `objectstack-ai#6307` | 1/1 | 1 | 0 | 0 | 0 | `293476148` | | `objectstack-ai#6316` | 6/3 | 6 | 0 | 0 | 0 | `448ac9565` | | `objectstack-ai#6345` | 10/3 | 10 | 0 | 0 | 0 | `e2798fab7` | | `objectstack-ai#6361` | 4/2 | 4 | 0 | 0 | 6 | `90bbf2510` | | `objectstack-ai#6363` | 6/2 | 6 | 0 | 0 | 2 | `17d095413` | | `objectstack-ai#6483` | 3/2 | 3 | 0 | 0 | 0 | `ee58392e1` | | `objectstack-ai#8722` | 1/1 | 0 | 0 | 1 | 0 | — | | `objectstack-ai#8724` | 1/1 | 1 | 0 | 0 | 0 | `ff4ba6a06` | | `objectstack-ai#8726` | 8/4 | 7 | 1 | 0 | 1 | `e783e163d` | | `objectstack-ai#8796` | 13/3 | 13 | 0 | 0 | 4 | `a4331227b` | | `objectstack-ai#8848` | 3/2 | 1 | 2 | 0 | 1 | `4fc4a3c0b` | | `objectstack-ai#8919` | 1/1 | 0 | 1 | 0 | 0 | `b5378550e` (held file) | | `objectstack-ai#9934` | 17/7 | 17 | 0 | 0 | 4 | `79c46da90` | | `objectstack-ai#9967` | 1/1 | 1 | 0 | 0 | 0 | `8f266f1cd` | | `objectstack-ai#10179` | 1/1 | 0 | 0 | 1 | 2 | — | | `objectstack-ai#10243` | 40/13 | 40 | 0 | 0 | 9 | `266436a7f`, `02b41232d` | | `objectstack-ai#10293` | 3/3 | 3 | 0 | 0 | 0 | `92a69d813` | | `objectstack-ai#10338` | 1/1 | 1 | 0 | 0 | 0 | `d2619fd0c` | | `objectstack-ai#10340` | 3/2 | 2 | 1 | 0 | 1 | `26f3588fb` | | `objectstack-ai#10380` | 12/2 | 12 | 0 | 0 | 0 | `dd8172ee2` | | `objectstack-ai#10485` | 3/3 | 3 | 0 | 0 | 0 | `35ad101bc` | | `objectstack-ai#10503` | 8/2 | 3 | 5 | 0 | 1 | `67ceb9aef` | | `objectstack-ai#10537` | 2/1 | 2 | 0 | 0 | 0 | `e634ecf6a` | | `objectstack-ai#10554` | 1/1 | 1 | 0 | 0 | 0 | `6abc4df03` | | `objectstack-ai#10629` | 75/23 | 75 | 0 | 0 | 0 | `13a6cb4ad` | | `objectstack-ai#10630` | 4/1 | 4 | 0 | 0 | 0 | `dd8172ee2` | | `objectstack-ai#10789` | 2/1 | 2 | 0 | 0 | 1 | `38bc74ed1` | | `objectstack-ai#10886` | 1/1 | 1 | 0 | 0 | 1 | `809e61221` | | `objectstack-ai#10888` | 3/3 | 2 | 1 | 0 | 1 | `d806081dd` | | `objectstack-ai#10961` | 5/3 | 5 | 0 | 0 | 3 | `222d06fc1` | | `objectstack-ai#10965` | 2/1 | 2 | 0 | 0 | 1 | `ab47f6974` | | `objectstack-ai#10978` | 1/1 | 1 | 0 | 0 | 0 | `4c9780c7a` | | `objectstack-ai#10983` | 3/2 | 3 | 0 | 0 | 0 | `6a4e929f5` | | `objectstack-ai#11006` | 4/4 | 3 | 1 | 0 | 0 | `cccbe51bf` | | `objectstack-ai#11015` | 3/1 | 3 | 0 | 0 | 0 | `82cb6e849` | | `objectstack-ai#11166` | 8/3 | 8 | 0 | 0 | 4 | `735f5c709` | | `objectstack-ai#11333` | 1/1 | 1 | 0 | 0 | 0 | `ea4d16420` | | `objectstack-ai#11504` | 3/2 | 3 | 0 | 0 | 0 | `f90e82024` | | `objectstack-ai#11513` | 2/2 | 2 | 0 | 0 | 0 | `e170b0ae5` | | `objectstack-ai#11703` | 8/3 | 8 | 0 | 0 | 1 | `5cb62d88b` | | `objectstack-ai#12010` | 1/1 | 1 | 0 | 0 | 0 | `77b91bdb4` | | `objectstack-ai#12176` | 5/5 | 5 | 0 | 0 | 0 | `7986d973f` | | `objectstack-ai#12194` | 11/4 | 8 | 3 | 0 | 0 | `311433f6b` | | `objectstack-ai#12195` | 9/4 | 4 | 5 | 0 | 10 | `7986d973f` | | `objectstack-ai#12281` | 20/5 | 20 | 0 | 0 | 5 | `0783d7b80` | | `objectstack-ai#12943` | 7/3 | 7 | 0 | 0 | 0 | `090f2302e` | | `objectstack-ai#13037` | 8/2 | 8 | 0 | 0 | 5 | `e7dfb1d69` | | `objectstack-ai#13233` | 5/1 | 5 | 0 | 0 | 0 | `3800e4293` | | `objectstack-ai#13241` | 5/4 | 5 | 0 | 0 | 1 | `a21d2a9cf` | | `objectstack-ai#13273` | 11/3 | 11 | 0 | 0 | 0 | `3a86a65e7` | | `objectstack-ai#13279` | 3/2 | 3 | 0 | 0 | 0 | `6a180e42d` | | `objectstack-ai#13325` | 3/1 | 3 | 0 | 0 | 0 | `2e0b7b18f` | | `objectstack-ai#13644` | 5/4 | 5 | 0 | 0 | 1 | `34ce8e7db` | | `objectstack-ai#13657` | 13/1 | 13 | 0 | 0 | 1 | `b003cf2e8` | | `objectstack-ai#14143` | 26/8 | 26 | 0 | 0 | 4 | `f19475c0a` | | `objectstack-ai#14390` | 1/1 | 1 | 0 | 0 | 0 | `9d7f7259f` | | `objectstack-ai#14398` | 3/1 | 3 | 0 | 0 | 0 | `317132495` | | `objectstack-ai#14403` | 6/1 | 6 | 0 | 0 | 0 | `93d2d679b` | | `objectstack-ai#14421` | 2/1 | 2 | 0 | 0 | 0 | `bd8795ea1` | | `objectstack-ai#14422` | 4/2 | 4 | 0 | 0 | 4 | `dc7c226b9` | | `objectstack-ai#14423` | 3/1 | 3 | 0 | 0 | 1 | `a56baa2bd` | | `objectstack-ai#14474` | 1/1 | 1 | 0 | 0 | 0 | `df657d9df` | | `objectstack-ai#14667` | 2/1 | 2 | 0 | 0 | 0 | `dc7c226b9` | | `objectstack-ai#14678` | 2/1 | 2 | 0 | 0 | 2 | `73ad0bba7` | | `objectstack-ai#14683` | 2/2 | 2 | 0 | 0 | 0 | `96326040f` | | `objectstack-ai#14723` | 1/1 | 1 | 0 | 0 | 0 | `65846bc46` | | `objectstack-ai#14745` | 1/1 | 0 | 0 | 1 | 0 | — | | `objectstack-ai#14748` | 1/1 | 1 | 0 | 0 | 1 | `92b5d7f00` | | `objectstack-ai#14758` | 15/5 | 15 | 0 | 0 | 1 | `84199cb87` | | `objectstack-ai#14760` | 6/2 | 6 | 0 | 0 | 2 | `ee32e1cb8` | | `objectstack-ai#14864` | 3/3 | 3 | 0 | 0 | 3 | `066dd3bd0` | | `objectstack-ai#14878` | 2/1 | 2 | 0 | 0 | 1 | `29db3cd2a` | | `objectstack-ai#14908` | 3/2 | 3 | 0 | 0 | 0 | `d5cbb44f3` | | `objectstack-ai#14921` | 2/1 | 2 | 0 | 0 | 0 | `c1d274de7` | | `objectstack-ai#15063` | 2/1 | 2 | 0 | 0 | 0 | `ad35745e8` | | `objectstack-ai#15068` | 2/2 | 2 | 0 | 0 | 4 | `8744de9e9` | | `objectstack-ai#15071` | 5/2 | 5 | 0 | 0 | 0 | `cf6e0a193` | | `objectstack-ai#16610` | 3/1 | 3 | 0 | 0 | 0 | `316a20fc5` | | `objectstack-ai#16649` | 4/1 | 4 | 0 | 0 | 0 | `44c917a47`, `613bfbd3d` | | `objectstack-ai#16755` | 1/1 | 1 | 0 | 0 | 0 | `44c849c7d` | | `objectstack-ai#16758` | 1/1 | 1 | 0 | 0 | 0 | `6e9bee640` | | `objectstack-ai#16783` | 1/1 | 1 | 0 | 0 | 0 | `854639b31` | | `objectstack-ai#16919` | 1/1 | 1 | 0 | 0 | 0 | `2cd4c548e` | | `objectstack-ai#17038` | 1/1 | 0 | 0 | 1 | 0 | — | | `objectstack-ai#17039` | 1/1 | 1 | 0 | 0 | 0 | `edf59e359` | | `objectstack-ai#17041` | 2/2 | 0 | 0 | 2 | 0 | — | | `objectstack-ai#17114` | 2/2 | 2 | 0 | 0 | 2 | `4af758d47` | | `objectstack-ai#17147` | 1/1 | 1 | 0 | 0 | 0 | `aaacf1d5c` | | `objectstack-ai#17148` | 1/1 | 0 | 0 | 1 | 0 | — | | `objectstack-ai#17195` | 1/1 | 1 | 0 | 0 | 0 | `d2c1d1980` | | `objectstack-ai#17219` | 1/1 | 1 | 0 | 0 | 0 | `706ad0fcc` | | `objectstack-ai#19364` | 2/2 | 2 | 0 | 0 | 0 | `ada701220` | | `objectstack-ai#19394` | 5/2 | 5 | 0 | 0 | 0 | `0862063ba` | Every cited sha matches exactly one object (`git rev-parse --disambiguate`, count 1 for each of the 95), is a commit, has one parent, and is an ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 95). The checkout is not shallow (`--is-shallow-repository` false), and the control leg `13a6cb4ad` exits 0 too. **Numbers with more than one anchor, by site:** - `objectstack-ai#10243` (40 sites): `266436a7f` for the 26 sites that describe the 2026-08-23 ruling it implements (the enablement door joins the `manage_metadata` write set, with the `trigger` exclusion), and `02b41232d` for the 14 that name the leak itself (「the leak commit 02b4123 measured」). That commit recorded the measurement over HTTP and says it is part of that card. - `objectstack-ai#16649` (4 sites): `613bfbd3d` for the first half (the fourteen remaining `boot-refusal` rows registered) and `44c917a47` for the second (the face refusal widened to every published package, and `boot-refusal` retired). - `objectstack-ai#12176`, `objectstack-ai#12194`, `objectstack-ai#12195`: the stages of one ruled retirement. `311433f6b` is stage 1 (the item-name grammar refused at the publish door) and `7986d973f` is stage 3 (the compound arities un-mounted). These are the anchors the spec stages gave. **Wordings to check, each true of its commit:** - `objectstack-ai#10293` (3 sites) cited the p1 flake whose signature had the expected-noise lines lifted into it. They now read 「(a vitest teardown race, fixed by commit 92a69d8)」. `92a69d813` names that number in its subject and fixed the flake by disarming vitest's console-forwarding teardown race, which is why the noise pointed the dispatch at the wrong mechanism. - `objectstack-ai#16755` and `objectstack-ai#16783` each cited an open PR that held a file at the time. They now read 「the change that landed as commit 44c849c held that file」 and 「then held by the change that landed as commit 854639b」. Each commit's diff edits the named file (`domains/automation.ts`, `seed-loader.test.ts`). - Quoted rulings keep their words. `dispatcher-plugin.declared-5xx-prose-withhold.test.ts:13` and `dispatcher-plugin.declared-user-message.test.ts:35` quote the 2026-08-27 ruling, and `dispatcher-5xx-demoted-code-withhold.test.ts:281` quotes an older note. There the commit stands in an editorial bracket (`[commit 79c46da]`, `[commit 0783d7b]`) in place of the number. - `objectstack-ai#9934`'s 「second constraint」 and 「third constraint」 now read 「the ruling's second constraint, commit 79c46da」. That commit's own diff calls status-agnosticism 「the ruling's second constraint」. - `domains/packages.ts:841` read 「declares, since objectstack-ai#19364:」 above the `enabled` line, but that line predates `ada701220` (objectstack-ai#19364's commit). It now reads 「declares — a key commit ada7012 kept rather than retired:」. - `route-ledger.ts:288`: 「objectstack-ai#16758 filed the second kind」 now reads 「Commit 6e9bee6 gated the second kind」, because that commit added the row census after the index-slice incident the sentence goes on to describe. - `flow-clone.ts:7` and `domains/automation.ts:2403` cite `e170b0ae5` for objectstack-ai#11513: the commit that landed 「lock package-declared permission sets at the save door; clone to customize」, whose changeset names the number. ## The sites left **No deciding commit, or a literal reader (8 sites):** - `api-exposure.ts:108` (objectstack-ai#6259): `api-exposure.test.ts:152` splits this `@param` block on the literal `'objectstack-ai#6259'`, so rewriting the comment would change what the test measures. Its deciding commit is `6968885ef`, which the three test-comment sites of the same number now cite. - `domains/mcp.ts:360` (objectstack-ai#8722): a wider contract change 「archived unscheduled」. It never landed, so no commit decided it. - `domains/meta-state-plural-tolerance.test.ts:130` (objectstack-ai#10179): an untaken option on a tracking card. The only commit naming the card, `53a48c93f`, recorded the opposite state. - `package-door-namespace-conflict-code.test.ts:30` (objectstack-ai#14745): a residue item on a review card. The only commit carrying the token is the one that added this file. - `route-ledger.conformance.test.ts:33` (objectstack-ai#17038): an ablation measured on a PR whose squash commit, `6a7910abb`, neither records nor performs it. - `route-ledger.conformance.test.ts:38` and `route-ledger.ts:300` (objectstack-ai#17041): a maintainer decision the lines call open. - `security/artifact-granted-permissions.test.ts:291` (objectstack-ai#17148): a question the line itself says is unsettled. **Held with `domains/meta.ts` (20 sites), anchors verified for the follow-up:** `objectstack-ai#8726` `:116` to `e783e163d`; `objectstack-ai#8848` `:200`, `:1398` to `4fc4a3c0b`; `objectstack-ai#8919` `:1247` to `b5378550e`; `objectstack-ai#10340` `:1309` to `26f3588fb`; `objectstack-ai#10503` `:14`, `:1143`, `:1159`, `:1250`, `:1308` to `67ceb9aef`; `objectstack-ai#10888` `:1337` to `d806081dd`; `objectstack-ai#11006` `:103` to `cccbe51bf`; `objectstack-ai#12194` `:831`, `:1050`, `:1173` to `311433f6b`; `objectstack-ai#12195` `:819`, `:827`, `:1046`, `:1167`, `:1960` to `7986d973f`. PR objectstack-ai#20615's one hunk there is at `:1874`, disjoint from these lines, but the rule is file-level. **String sites kept as tokens (100).** 95 are test titles and test-code strings in 43 files. Five are non-test strings: the `route-ledger.ts` `note` fields at `:435`, `:441` and `:505`, a string at `dispatcher-error-vocabulary.ts:349`, and the enablement door's refusal text at `domains/activation-gate.ts:279`, which ends 「(objectstack-ai#10243).」 (see Acceptance notes). ## Mechanical guard: no code token moves The check compares the TypeScript parser's leaf tokens (TypeScript 6.0.3, JSDoc nodes excluded, so template literals are read in context) of each touched file at base `eb4b17c346` against the working tree at `a5cdfd8a46`, over all 118 touched `.ts` files. Controls mutate the head text in memory only, so nothing on disk moved for them. - Real run: 301,081 base tokens, **0 files with a token change** (exit 0). - Comment-insertion control (`domains/activation-gate.ts`): 0 files changed (exit 0). - Code-insertion positive control (a declaration in the same file): DIFFER at token 34 (exit 1). - String positive control (`(objectstack-ai#10243)` to `(objectstack-ai#10244)` inside the kept refusal string): DIFFER at token 339 (exit 1). Line balance: every touched file is +N/−N (508/508), and every line count is equal at base and head. A raw scan of the 119 changed files for control bytes finds none. ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/runtime` is included, in PR objectstack-ai#20609's form and level. It says only that the provenance comments were re-anchored. Measured on the built package: `files[]` is `dist`, `README.md` and `CHANGELOG.md`. After `pnpm --filter @objectstack/runtime build`, the rewritten docblocks reach `dist`: for example `e2798fab7` appears 3 times and `68f5eccb1` 6 times in `dist/index.d.ts`, and `f19475c0a` 4 times in `dist/index.js`. The positive control, the unchanged sentence 「drags `@libsql/client` (native bindings included)」 of the same `turso-driver-factory.ts` docblock, is in `dist/index.d.ts`, and a negative control phrase appears nowhere. The only dead number left in `dist` is the kept refusal string's `objectstack-ai#10243`. ## Gates (head `a5cdfd8a46`) This host has no `flock`, so `os-verify-lock.sh` ran in its declared unlocked mode. Its disclosure, verbatim, from each locked run at this head: ```text os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 5s · declare it in the PR body · pnpm --filter @objectstack/runtime build os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 99s (1m39s) · declare it in the PR body · pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 6s · declare it in the PR body · pnpm --filter @objectstack/runtime exec vitest run --project repo --maxWorkers=2 os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 8s · declare it in the PR body · pnpm --filter @objectstack/runtime typecheck ``` The dependency closure and the whole workspace were built first, at the merge head `ca6d13d6ab`, the same way: `turbo run build --filter='@objectstack/runtime...'` (30 tasks, exit 0) and `turbo run build --filter='./packages/*' --filter='./packages/*/*'` (71 tasks, exit 0). `a5cdfd8a46` differs from that head only in `domains/meta.ts`, which went back to base bytes, and `@objectstack/runtime` was rebuilt at `a5cdfd8a46`. - **Tests:** `vitest run --project local`: 288 files, 4,190 tests passed, 1 skipped. `--project repo` (which holds the touched `action-owner-key-single-source.test.ts`): 3 files, 727 tests passed. Together they cover every touched test file. - **Typecheck:** `pnpm --filter @objectstack/runtime typecheck` exits 0. `tsc --listFiles` counts 82 `src` files (no tests) under `tsconfig.json` and all 291 test files under `tsconfig.test.json`, which `check:test-typecheck` judges: 27 files, 190 errors, 68 pinned signatures held. - **Lint:** the repo-wide `pnpm lint` (`eslint . --no-inline-config`) exits 0 at `a5cdfd8a46` (2026-09-29T09:23:06Z to 09:23:36Z). A narrowed run over the 118 touched `.ts` files through eslint's API agrees: 118 linted, 0 ignored, 0 errors, 0 warnings. - **Citation judging:** `node scripts/check-issue-citations.mjs --base origin/main` exits 0. The diff-scoped run judged 23 citations across 28 files, and all 23 resolve. These are the live numbers that stay on rewritten lines. It defers `*.test.ts`, so the added-minus-removed count over the whole diff covers the rest: 0 numbers added. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at `a5cdfd8a46` derived 67 families, the same set as at the merge head. All 67 exit 0. `--ran` reads 「67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN」. - At the merge head, `check:dual-build-cjs-loads` and `check:type-check-debt` first exited 3 (PREREQUISITE NOT MET) on a partly built workspace. After the whole-workspace build both exited 0, and both exit 0 at the final head. - Among them: `check:doc-authoring` (the sibling prose-id baseline holds, 810 pinned sites, no growth), `check:nul-bytes` (9,250 files, no raw control bytes), `check:route-ledger-census`, `check:dispatcher-error-vocabulary` and `check:issue-citations` (self-test, 114 cases in 8 batteries). - **Artifact rosters:** 38 of the 41 non-self-test roster rows exit 0 at the merge head. The other three, `check-closing-target-claim`, `check-partof-closing-keyword` and `check-single-claim-paths`, answer 「NOT WIRED」 (exit 2) without a pull request's context, and are run against this PR and reported on the card. ## Hypotheses (measured first) - **H0 holds.** The filtered census answers 217 dead sites at `eb4b17c346` (29 files, 59 numbers), equal to the card's count at `f11b5f20a2`: no drift. - **H1 holds, with the listed exceptions.** After the rewrite the filtered census answers 23: the 20 sites held with `domains/meta.ts` for an open PR, and 3 deliberate ones (a literal reader, a card never landed, an open decision). The supplementary reading adds 5 test-comment sites of the same two kinds. - **H2 holds, by the token guard.** A comment-stripped comparison of every touched file (the parser's leaf tokens, JSDoc excluded) is empty, and its controls fire. The emitted `dist` is not byte-identical, because the docblocks ship, which is why the changeset is `patch`. ## Acceptance notes - **The held file.** The claim's read (07:51Z) and this stage's first read of the open PRs' file lists (08:06:32Z, 8 open PRs) found none touching `packages/runtime/src`. PR objectstack-ai#20615 opened at 08:12:40Z and edits `domains/meta.ts`. The re-read at 09:07:08Z (7 open PRs) found it, and it is the only open PR touching the package. The file went back to its base blob in `a5cdfd8a46`, and `git hash-object` equals `b4ddb362cc`, the blob at the base and at `origin/main`. The 20 anchors above are ready for the follow-up once that PR lands. - **Form D, not touched here.** `domains/activation-gate.ts:279` is part of the enablement door's refusal message and ends 「(objectstack-ai#10243).」. An author sees it, so it is ruling D's (no number, the lesson in words), a string change outside this comment-only scope. It needs a form-D carrier. The other four non-test string sites are ledger `note` data and a gate's own string. - **The grammar does not read a slash-joined number.** `CITATION_RE` refuses a `#` preceded by `/`, so the second number of `#A/#B` is never judged. In `packages/runtime/src`, 3 such dead numbers exist (`objectstack-ai#10630`, `objectstack-ai#12281`, `objectstack-ai#12194`), and all 3 are rewritten here. The other 36 distinct slash-joined numbers there were probed by REST and answer 200. One more dead one, `objectstack-ai#17219`, stands slash-joined inside a test title, a string, and is kept. This is the same shape as PR objectstack-ai#20612's slash-joined `objectstack-ai#5775/objectstack-ai#6629`. It is noted, not filed. - **Outside the scope and the census surface.** `packages/runtime/vitest.config.ts:54` cites `objectstack-ai#17853`, which answers 404. The file is outside `src/**`, so it is left for whoever owns the package's config. The other numbers there, and those in `tsup.config.ts` and `README.md`, answer 200. - **Base.** The branch merged `origin/main` once (`ca6d13d6ab`, merging `c1d8051e0a`) before the `--base origin/main` run, as the dispatch orders. That merge brought PR objectstack-ai#20609's and PR objectstack-ai#20612's landed stages and touched none of this diff's files. `origin/main` has since moved to `ed6f7348f9`, one commit that touches only `packages/cli`, so there was no second merge. - **Anchors shared with the landed stages.** 24 numbers keep the anchor the spec, lint or service-messaging stages already gave them, for example `f19475c0a` (objectstack-ai#14143), `b003cf2e8` (objectstack-ai#13657), `311433f6b` (objectstack-ai#12194), `8e13ca876` (objectstack-ai#6206) and `17d095413` (objectstack-ai#6363). ## Deviations - Three changed lines hold only a slash-joined dead number, beyond the census's sites (see Acceptance notes). Five more are the other half of a rewritten sentence (listed under What changed). - Commit trailers are AGENTS.md's model-free pair (`Claude-Session` plus `Co-authored-by: Claude`), and the pre-push trailer check passed on every push. The merge commit carries git's default message. --- _Generated by [Claude Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_ --------- Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
…mits that decided them (objectstack-ai#20626) Part of objectstack-ai#20596 Clause-②: no ## What changed This is the second stage of the `domain:services` lane of the dead-citation sweep. It covers `packages/plugins/plugin-sharing/src/**` and nothing else. By census, it is the largest package in the lane that no open PR or in-flight claim holds (the claim, `5886159115`, gives the order). Later stages cover the other packages, so this PR says `Part of` and the card stays open. Every comment or docblock site in scope that cited a tracker number answering 404 has been rewritten in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), by stage 1's method (PR objectstack-ai#20609, landed as `422db788a`). That is **87 sites on 86 lines in 23 files, covering 13 numbers**: the 60 census sites outside the generated headers, and 27 sites in test comments, which the census defers. Each rewritten line now cites the commit in `origin/main` history that decided what the line describes, and it says in its own words what that commit decided. No ADR or ruling-record file records the decision behind any of the 13 numbers (ADR-0131 names objectstack-ai#14484 only as evidence, not as the record of its ruling), so every anchor is a commit: **13 distinct shas**. No number was dropped. Only comments changed. Every touched source file keeps its line count (87 lines out, 87 in, over 23 files), so no line citation into these files moves. One of those 87 lines held no dead citation: `backfill-sys-record-share-organizations.ts:14`, where 「the cliff the card names」 lost its referent once line 10 named a commit instead of a card. It now reads 「the cliff that commit pins」, and `3f64fe6c6`'s backfill test is the one titled 「the cliff」. No code token moves (see the guard below). **No citation number is added.** Every tracker number on an added line was already on the line it replaces. Over the whole diff, added minus removed is 0 or negative for every number, and no number is new to the diff. No PR number stands on an added line. The one PR spelling in scope (`PR objectstack-ai#5973`, dead) became its squash commit. Nineteen dead sites are left on purpose: 1 string literal, 14 test titles, 1 verbatim ruling quotation and 3 generated file headers (see the list below). One more file: a `patch` changeset for `@objectstack/plugin-sharing`, because the rewritten docblocks ship (see Changeset below). ## Census: `plugin-sharing`, before and after **Instrument (A1).** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged. The count below is its `allocated-but-absent` findings under `packages/plugins/plugin-sharing/`. Each run counts as a reading only because its board frontier equals the newest issue number, read by a separate request just before and just after the run. | reading | tree | board | whole-repo `allocated-but-absent` | plugin-sharing sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `422db788a`, run 2026-09-29T08:04:49Z to 08:08:19Z | enumerated, 185 pages, frontier objectstack-ai#20614 (newest objectstack-ai#20614), 18,441 numbers | 2,300 | **63** | 62 | 14 | 13 | | after | head `a6d231713`, run 08:27:44Z to 08:31:07Z | enumerated, 185 pages, frontier objectstack-ai#20616 (newest objectstack-ai#20616), 18,443 numbers | 2,240 | **3** | 3 | 3 | 1 | The before count matches the 63 that census `5884031174` read at `f11b5f20`. The whole-repo drop is 60, exactly this diff's census sites. The `resolves` tally is 32,803 in both runs, and `resolves-as-pull-request` (1,891) and `cross-repo-unjudged` (983) did not move either. The 3 left are the generated headers below. No run was truncated or discarded: all three enumerations in this stage (two census runs and the supplementary board below) read 185 pages at the newest frontier. **Supplementary instrument, the whole scope.** The census does not read test files or strings, and this stage's scope includes both. So a second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) and `classifyCitation` over every `.ts` file under `plugin-sharing/src` (74 files). It uses one board, enumerated by the gate's own `enumerateBoard` at 08:12:18Z (185 pages, frontier objectstack-ai#20614, equal to the newest). | reading | citations | dead | src comment | test comment | src string | test string | |---|---|---|---|---|---|---| | before, `422db788a` | 1,187 | **106** | 63 | 28 | 1 | 14 | | after, `a6d231713` | 1,100 | **19** | 3 | 1 | 1 | 14 | Its src-comment column equals the census's 63, which is the control on the second instrument. The 989 resolving, 87 pull-request and 5 cross-repo citations are the same in both readings. ## Per-number table Sites and files count all dead sites in scope at the base (comments and strings, tests included). `rewritten / left` counts the sites rewritten and the sites left. Each anchor was read in its message and diff, not only its subject. It is the commit that decided what the line describes: its own message or diff names the number it replaces, or, for a squash-merged PR, it is the merge of that PR. | number | sites / files | rewritten / left | anchor: what it decided | |---|---|---|---| | `objectstack-ai#5973` | 4/2 | 3/1 | `abeb3751f`: `HierarchyScopeContext.organizationId` is the tenancy authority, and it is required. `objectstack-ai#5973` was the PR itself; this is its squash commit | | `objectstack-ai#6206` | 12/7 | 11/1 | `8e13ca876`: the share-link routes hand enforcement the whole authz envelope, per maintainer ruling A of 2026-08-07. It is the plugin-sharing half; the spec stages anchor the contract half at `d7e0b4212`. Three sites name the ruling in words, 「the full-envelope ruling」, beside `aa4b90d9a`, which applied it | | `objectstack-ai#6523` | 3/3 | 3/0 | `aa4b90d9a`: 36 contract signatures converge on the full `ExecutionContext`. The same anchor the spec stages gave this number | | `objectstack-ai#8710` | 10/3 | 9/1 | `04d03c3a0`: a deactivated `sys_position` confers no sharing-rule shares. Its message quotes the 2026-08-15 ruling: access-conferring paths filter, addressing paths do not | | `objectstack-ai#8792` | 2/1 | 2/0 | `83c661d97`: the bulk-write merge's missing provenance mark is recorded as ruled (2026-08-15), not oversight | | `objectstack-ai#8836` | 1/1 | 1/0 | `1850ebbb0`: it pins 「no filter object that can be vouched 'author' may outlive the request that vouched it」, the invariant the line names. The same anchor the spec stages gave this number | | `objectstack-ai#11671` | 5/5 | 2/3 | `09b4f4e4e`: `os i18n extract --source-hashes` writes the provenance companion (maintainer ruling objectstack-ai#12069 Option A, which stays cited). The same anchor stage 1 gave it | | `objectstack-ai#11674` | 4/2 | 4/0 | `1cba33f16`: the seed loader warns at load time when a required column is deferred, and the ordering constraint is written at the four pointer-pair sites, these two among them | | `objectstack-ai#12493` | 2/2 | 2/0 | `aa5994e17`: the Operation Message Catalog gains `record_write_denied` ahead of its emitters. The same anchor the spec stages gave this number | | `objectstack-ai#13279` | 3/2 | 3/0 | `6a180e42d`: a permission-store read that throws raises `AuthzStoreUnavailableError` (503), and each transport re-raises it rather than laundering it into a 401 | | `objectstack-ai#13398` | 1/1 | 1/0 | `953a81f4a`: the class ruling on published logger sinks, applied at this site. `error` is reachable only because the sink already declares it; growing `error?` onto a published sink is forbidden. No record of the ruling exists in the repo, and this commit, which wrote this heading, is its earliest application in history | | `objectstack-ai#13608` | 23/3 | 21/2 | `fc9ba76a5`: `publicSharing.eligibility` is held at redemption, not only at mint. The same anchor the spec stages gave this number | | `objectstack-ai#14484` | 36/8 | 25/11 | `3f64fe6c6`: `organization_id` is stamped on every `sys_record_share` write, the stranded rows are backfilled, and the object is admitted to the tenancy ledger (the 2026-09-02 ruling, decision batch objectstack-ai#11 item 3) | Every cited sha matches exactly one commit (`git rev-parse --disambiguate`, count 1 for each), and every one is an ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 13). The history is complete (`--is-shallow-repository` false, 15,073 commits). A line-origin pickaxe (`git log -S` on each dead line's exact text) found each line entering either in its anchor commit or in a later commit that cites that commit's decision. For example, `65759baca` is the consumer half that cites `aa5994e17`'s key, and `b70a55d62` cites `3f64fe6c6`'s ledger admission. Wordings to check, each true of its commit: - `backfill-sys-record-share-organizations.ts:5`: 「rows that `SharingService.grant`, before commit 3f64fe6, stranded」. `3f64fe6c6` is the writer fix, and this module is its backfill. - `backfill-sys-record-share-organizations.ts:36` and `:124`: 「the 2026-09-02 ruling commit 3f64fe6 applies (decision batch objectstack-ai#11 item 3, …)」. The verbatim maintainer quotation on line 37 is untouched. - `share-link-service.ts:841`: 「(published-sink level ruling, commit 953a81f)」. The heading's body already states the ruling (option C allowed, option B forbidden). - `exec-context-annotation.pin.ts:7-8`, `sharing-rule-service.ts:12-13` and `sharing-service.ts:20`: 「since commit aa4b90d (the full-envelope ruling: no per-site subset contracts)」. `aa4b90d9a`'s message: 「Apply the … ruling default (converge on the full envelope, keep no per-site subset contracts)」. - `share-link-routes.ts:81`: 「[commit 8e13ca8, full-envelope ruling]」, so that 「the whole point of the ruling」 five lines down still has a referent. ## The 19 sites left - **Non-test string (1 site).** `sharing-service.ts:1674` sits inside the operator-facing `warn` text for a hierarchy scope that was not widened (「… resolveOwnerIds, objectstack-ai#5973); …」). It is a runtime string, so it is form D, not form C, and the shrink-only `doc-authoring-prose-id` baseline already holds it (`sharing-service.ts` → `objectstack-ai#5973: 1`). Left and listed, as stage 1 left its refusal strings. - **Test titles (14 sites).** `describe` titles in `backfill-sys-record-share-organizations.test.ts:185`, `:274`, `:324`, `:367`, `record-share-organization-stamp.test.ts:194`, `:239`, `:278`, `:315`, `:353`, `:436`, `sharing-service.test.ts:1798` (the `objectstack-ai#14484` titles), `share-link-eligibility.test.ts:607` (`objectstack-ai#13608`), `share-link-enforcement-context.test.ts:226` (`objectstack-ai#6206`) and `sharing-rule.test.ts:1898` (`objectstack-ai#8710`). Tokens, left as they were. - **A verbatim ruling quotation (1 site).** `share-link-service.test.ts:478` is point 2 of the maintainer's 2026-09-01 ruling, quoted verbatim and untranslated. It carries 「沿 objectstack-ai#13608 先例」. AGENTS.md keeps a quoted Chinese ruling in its original words, and rewriting the quote would rewrite the ruling. Left. - **Generated headers (3 sites).** Line 8 of the `es-ES`, `ja-JP` and `zh-CN` `.source-hashes.generated.ts` files carries 「(objectstack-ai#11671, maintainer ruling objectstack-ai#12069 Option A, extending objectstack-ai#8765 Option B)」. `os i18n extract` writes that line from `packages/cli/src/utils/i18n-extract.ts`, so the fix belongs at the producer, the carrier stage 1 named. The hand-written `translations/index.ts:26` is rewritten here, with the same wording stage 1 used. ## Mechanical guard: no code token moves The guard compares the TypeScript parser's leaf nodes, with comments as trivia and JSDoc nodes excluded, base `422db788a` against head. Template literals are therefore read in context. It ran over all 23 touched `.ts` files. - Real run: 96,665 base tokens, **0 files with a token change** (exit 0). - Comment-insertion control in `share-link-service.ts`: 0 files changed, as expected (exit 0). The first attempt was a no-op: its replacement still contained the anchor, so `scripts/ablation-replace.mjs` refused it before the guard ran. It was redone with an anchor the replacement does not contain. - Positive control, a code token changed in `share-link-service.ts` (`Boolean(eligibility),` to `Boolean(eligibility) && true,`): DIFFER (exit 1). - Positive control, one digit changed inside the kept `sharing-service.ts:1674` warn string: DIFFER (exit 1). Every mutation went through `scripts/ablation-replace.mjs`. Each restore was proven byte-identical to the HEAD blob (`ba7fba2e8199`, `2833b9a1616d`), with `git diff HEAD` empty and a clean tree afterwards. ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/plugin-sharing` is included. It says only that the provenance comments were re-anchored. Measured on the built package (A3): `files[]` is `dist`, `README.md` and `CHANGELOG.md`. After the build, the rewritten comments reach both halves of `dist`: `3f64fe6c6` appears 6 times in `dist/index.d.ts` and 8 in `dist/index.js`, `fc9ba76a5` 3 and 3, `04d03c3a0` 2 and 2, `8e13ca876` twice in `index.d.ts`, and `1cba33f16` 4 times in `index.js`. esbuild keeps only some comments, so the positive controls are unchanged lines beside rewritten ones that shipped. `share-link-service.ts:891` is found once in each half, and `sharing-service.ts:1269` once in `index.js`. A never-written negative phrase appears nowhere. The only dead number left in `dist` is the kept `objectstack-ai#5973` warn string. ## Gates (head `a6d231713`) - **Citation judging, as CI runs it:** `pnpm check:issue-citations` (self-test) exits 0. `node scripts/check-issue-citations.mjs` exits 0: the diff-scoped run judged 9 citations across 11 files, and all 9 resolve. - **Doc authoring:** `pnpm check:doc-authoring` exits 0, with the sibling-package prose ids at their baseline and no growth. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `a6d231713` (re-derived after a fresh `git fetch` at 09:58Z: the same 65, and none of the 8 new `main` commits touch anything it derives from) derived 65 commands. They include all 50 derived at dispatch, plus 15 more. All 65 exit 0. `--ran` reports 65 run, 0 NOT MEASURED, 0 unrun, and exits 0. - Three gates first exited 3 (PREREQUISITE NOT MET) because the workspace was only partly built: `check:dual-build-cjs-loads`, `check:i18n` and `check:type-check-debt`. A full `turbo run build` of `./packages/*` and `./packages/*/*` then ran under the shared verify lock (71 tasks, exit 0), and all three exited 0 on their rerun. `check:dts-closure`, `check:sourcemap-no-sources-content` and `check:lean-entry-closure` were rerun too, over 71, 68 and 15 built packages, and exited 0. - **Tests and typecheck, under the verify lock:** - `pnpm --filter @objectstack/plugin-sharing test`: 37 files and 913 tests pass. That is every test file in the package, the 12 touched ones included. - `pnpm --filter @objectstack/plugin-sharing typecheck` exits 0. Its main `tsc` program reads the 37 non-test files, and its `check:test-typecheck` program (`tsconfig.test.json`) reads all 74 files under `src/`, the 37 test files included (`--listFiles`). - **Lint, as a proven narrowing:** `eslint --no-inline-config --format json` over the 23 touched `.ts` files gives 23 files, 0 errors and 0 warnings. All 23 are in eslint's own population (`isPathIgnored` is false for each). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, as its own line 328 states), so a comment edit here cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's run. - **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of the 24 changed files for control bytes finds none. ## Acceptance notes - **The census instrument did not truncate in this stage.** Three enumerations read 185 pages each at the newest frontier. The truncation stage 1 saw (1 run in 5) is carried on objectstack-ai#20556, and this stage changes no instrument. - **What stays for later stages.** - The 3 generated `objectstack-ai#11671` headers, whose producer is `packages/cli/src/utils/i18n-extract.ts`. - The `objectstack-ai#5973` warn string (form D, held by the `doc-authoring-prose-id` baseline), the 14 test titles and the verbatim ruling quotation. - **Anchors the next stages can reuse.** The same numbers stand elsewhere in `packages/**/src`: `objectstack-ai#6206` at 53 sites and `objectstack-ai#11674` at 46 (the ordering-constraint note has two sibling copies outside this package, in `sys-approval-request.object.ts` and `sys-audit-log.object.ts`). `8e13ca876` / `d7e0b4212` / `aa4b90d9a` and `1cba33f16` are the anchors used here. - **Base.** The branch is 8 commits behind `origin/main` (`1322cc72c`, read at 09:58Z). None of them touches `plugin-sharing` or this changeset, and none re-anchors any of these 13 numbers, so there was no merge. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20580
Clause-②: yes (widening)
What was wrong
When an administrator explains another user's access, the explanation is computed in the administrator's own organization (#20515). Enforcement does one more thing for that same user first. Under a walled tenancy posture (
isolated,group) it vets the organization the user's session claims: a claim that no current membership backs is dropped, and the user resolves with no active organization, so only their global grants apply (#15409, ruling B). The explainer never ran that check. For a user whose membership in the administrator's organization had ended, the explanation listed that organization's grants, and the verdicts they decide, while enforcement applied none of them. Enforcement was correct throughout; the explanation was wrong.What changes
@objectstack/core: the session arm's check becomes one exported function,vetOrganizationClaim(claimedOrganizationId, accessibleOrgIds, tenancyPosture). It returns the claim while a current membership backs it, or while no wall is enforced, andundefinedonce the claim is dropped.resolveAuthzContext's session arm now asks it through the identical boolean. There is no behaviour change: the 11 existing session-arm tests inresolve-authz-context.test.tspass unchanged.@objectstack/plugin-security:explainAccessForCallerasksvetOrganizationClaimabout the explained user (theiraccessible_org_ids, and the plugin's tenancy posture) and resolves them in the organization it returns. The explainer spells no membership rule of its own.buildContextForUser's signature is unchanged. Its doc now says who vets the organization it is handed.Landing point:
security-plugin.ts, as expected. The check was reachable only through a new core export. The only other exported path that runs it,resolveAuthzContextitself, would skip the explainer's ruled grants-cache bypass and would write a false "session claim dropped" log line.Evidence
Pin:
packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts.ObjectQLon better-sqlite3 and on sqlite-wasm, the real platform objects and the realSecurityPlugin.resolveAuthzContextwith a session that claimsorg_alpha, thenassembleExecutionContext, then afindthrough the middleware.isolated,group): the removed member's explanation lists noorg_alpha-scoped set, the same sets enforcement resolves for them. Itsobject_crudverdict isdenies, where their own read is refused 403PERMISSION_DENIED.single: there is no wall, so the claim stands on both faces.Ablation. The fix was committed first.
scripts/ablation-replace.mjsran it with EXIT, INT and TERM restore, plus a shell trap on an absolute path.Mutation: the vetted organization was replaced by the caller's organization, unvetted. That is the pre-fix resolution.
On disk: anchor count 1 went to 0, and the marker count was 1 during the run.
Result: 8 failed, 14 passed (of 22). All 8 red cases are the removed-member pins (2 drivers, 2 walled postures, 2 pins each). KEEP, the precondition and the
singlecontrol stayed green.Quoted:
Restore: blob equals HEAD (
6a86472402fd), andgit diff HEADis empty.First attempt: the tool refused it before anything ran. The replacement text already occurred on disk, so its count could not rise. Nothing was measured; the rerun used a unique marker.
Local verification
Recorded at HEAD
bd7b46777. Core's test and typecheck ran atcf18f3e99; the diff from there to HEAD touches only the plugin-security pin file.pnpm --filter @objectstack/plugin-security test: 146 files, 3098 passed, 23 skipped.pnpm --filter @objectstack/plugin-security typecheck: exit 0.check:test-typecheckOK.pnpm --filter @objectstack/core test: 56 files, 1522 passed.vitest run --project local src/security/resolve-authz-context.test.ts: 102 passed, including the 4 newvetOrganizationClaimcontract cases.pnpm --filter @objectstack/core test:repo: 3 files, 48 passed.pnpm --filter @objectstack/core typecheck: exit 0.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 67 commands (the 51 derived at dispatch, plus 16 for the changeset and test-layer kinds). Each exit code was captured before any pipe, and--ranreconciled them: 64 exit 0, 3 NOT MEASURED, 0 unrun.check:dual-build-cjs-loads,check:i18nandcheck:type-check-debt. Each exited 3 with PREREQUISITE NOT MET, because each needs most of the monorepo built, and the core change invalidates the build cache for nearly every package downstream of it. CI builds that closure before these steps.packages-orgless-grants-capability-gate,packages-vetted-org-source). Runtime's dependency closure was unbuilt here, so vitest reported "Failed to resolve entry for package" and ran no test. They are declared to CI.eslint.config.mjs'sfilesglobs, the ts and js family minusNEVER_LINTED.--print-configresolves a config for all 6 touched.tsfiles; the changeset.mdmatches no glob.--no-inline-config --format jsonover those 6 files, with 0 errors and 0 warnings.parserOptions.project, noprojectService). So this diff cannot move a verdict on an untouched file.vetOrganizationClaimhas no other occurrence in the repository. The two star re-exporters of core (runtime,plugin-hono-server) declare no such name.Acceptance notes
Clause-②).@objectstack/coregains one export,vetOrganizationClaim, with no behaviour change. The changeset grades coreminorand plugin-securitypatch. The line above is copied from the claim as it stands.buildContextForUserreturns none, andexplainAccessForCallersets none. Measured atc96beb27, better-sqlite3, one current member of the administrator's organization:isolated, Layer 0 answers deny on a tenant object: the explanation saysallowed: falsewith the fail-closed filter, while that member's own read is admitted.resolveDelegatorContextdoes for a delegator) changes a current member's explanation, which this card's ruled pin keeps unchanged. It is reported to the seat for plugin-security closeout (explain ≠ enforce): object-levelsecurity.explainanswersallowed: trueunder a row-level policy comparing two fields of no shared class, whilefindrefuses it withINVALID_FILTER/ 400 #20604, the explain-versus-enforce family card.tenancyservice registered, admission hands the resolver no posture (no claim is ever dropped), while plugin-security probesorg-scopingand can resolveisolated. The explainer reads the plugin's posture. In that composition the two could disagree. Both read the sametenancyservice when it is registered.Seat append (domain:services seat,
session_01XY5uCwTjZj7884yYtyur4H)Clause-②line above was corrected fromnotoyes (widening)by the seat, following the dev's deviation 2.@objectstack/coregains one export,vetOrganizationClaim, and the changeset already grades coreminor. The claim was corrected in place in the same act.Generated by Claude Code