docs(plugin-security): re-anchor the dead tracker citations to the commits and ADRs that decided them - #20658
Conversation
…mmits and ADRs that decided them 266 comment and docblock sites under packages/plugins/plugin-security/src cited 40 tracker numbers that answer 404. Each now cites the in-repo record that decided what the line describes, and says it in its own words: the ADR-0055 amendment for the chain composition, ADR-0094 D5-R for the conflict ruling it records, and otherwise the commit in this repository's history. Comments only: every file keeps its line count and no code token moves. Test titles, two logger strings and three generated translation headers keep their numbers. Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude <noreply@anthropic.com>
…re-anchor The rewritten docblocks ship in dist, so the package's published bytes change. Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 16 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 2e0daa0b36b2c61f01868b64444f32db7926d56a && git checkout 2e0daa0b36b2c61f01868b64444f32db7926d56a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9b402dbaed683bcbc9c273bb6792af4adc02744c f90c9b123640c202cee3d9b2d16477e30433c054 && git checkout -B drift-repro 9b402dbaed683bcbc9c273bb6792af4adc02744c && git merge --no-ff f90c9b123640c202cee3d9b2d16477e30433c054
node scripts/docs-audit/affected-docs.mjs --json 9b402dbaed683bcbc9c273bb6792af4adc02744c
|
…bject leaves that contradict their current en source (objectstack-ai#20684) Fixes objectstack-ai#20653 Clause-②: no ## What changed A translated leaf in a plugin's `src/translations/{ja-JP,es-ES,zh-CN}.objects.generated.ts` that a translator wrote by hand keeps its value when its `en` source changes later. This PR measures that set in the four plugin bundles the card names (`plugin-webhooks`, `plugin-audit`, `plugin-approvals`, `plugin-security`), then re-translates the leaves whose meaning now **contradicts** the current `en`: **18 leaves**, six paths in all three locales (ja-JP 6, es-ES 6, zh-CN 6). The triage-ordered leaf, `sys_webhook.fields.definition_json.help`, is the first commit. - Values only. No key is added or dropped, no `en` file is edited, and no provenance companion (`*.source-hashes.generated.ts`) changes: the repo's own tooling was run and writes nothing (measured below). - `.changeset/20653-stale-authored-plugin-bundle-leaves.md`: `@objectstack/plugin-webhooks`, `@objectstack/plugin-audit`, `@objectstack/plugin-security`, each `patch`. `@objectstack/plugin-approvals` has no change and is not named. - No new gate, per triage. No test pins any of the old or new strings. - Scope per the claim: the four plugin bundles only. The `platform-objects` metadata-forms leaves are the engine lane's card, objectstack-ai#20666, which remains open and is not touched here. ## The measurement (base `6bff748bb`, before any edit) ### Instrument PR objectstack-ai#20652's condensed instrument, ported with one change: the bundle directory comes from argv instead of the `platform-objects` constant. Population = every string leaf of the locale's objects bundle, minus echoes of the current `en` and paths recorded in `*.source-hashes.generated.ts`; last edit = the first-parent commit where the parsed value last changed; a leaf is a candidate when `en` at that commit differs from `en` today. Meaning is then judged by hand. Full history (the clone is not shallow: `git rev-parse --is-shallow-repository` answers `false`). Four widening checks ran beside it: - **Carve-out continuity.** These bundles were carved out of `packages/platform-objects/src/apps/translations/` at `44045721c` (ADR-0029 D8: webhooks, approvals, security) and `be1b9165f` (K2: audit). The condensed walk starts at the carve-out, so it dates every carried leaf there and reads the `en` of that day. The widened walk replays the platform-objects file's first-parent history (from its birth at `6bacbced2`, the horizon objectstack-ai#20652 covered) before the plugin file's own. It re-dated every candidate the condensed walk had dated at a carve-out commit (for example `definition_json.help`, to `7bb92056e`) and changed no candidate set. - **Merge side.** For a leaf last edited by a merge commit, `en` is also read at the second parent. 0 hits. - **Penultimate edit.** For each non-candidate, `en` at its previous edit, and whether `en` moved in the last-edit commit itself. Every flag where `en` moved EARLIER than the last edit was read by hand: `sys_webhook.fields.method.help`, `sys_position.fields.name.help` and `sys_position._actions.clone_position.params.name.helpText` already say what `en` says; es-ES `sys_position._actions.deactivate_position.confirmText` differs by rewording only; es-ES `sys_position.description` **contradicts** (it was edited at `4ea921ca8`, after `en` moved, and kept the old framing), so it is re-translated. - **Retired terms and renamed keys.** A leaf carrying the locale's word for `project` or `department` where `en` at the same path does not carry the English term; the locale's word for `role` or `RBAC` anywhere in the four bundles; and a leaf whose path was born at its last edit while the same value sat under a sibling path of the same object that disappeared in that commit (a renamed key). One path, found by both the term scan and the renamed-key check: `sys_activity.fields.environment_id.help`. The key was renamed from `project_id` at `944f18758` with the value carried, so the since-last-edit instrument dates it at the rename, where `en` already said Environment, and cannot see it. ### Known-positive control `plugin-webhooks` `sys_webhook.fields.definition_json.help`, line 72 of all four bundles. The source has said "Credentials are NOT stored here" since `160294963` (the custom headers moved onto the encrypted channel, after `e3a6f6e7e` moved the signing secret), and the `en` bundle since `8af76aebf`. The three translations were last edited at `7bb92056e`, when `en` said "full headers/auth/retry/payload config". The ported instrument flags it in **all three** locales. A1 held: the port is live before anything is counted. ### Counts per bundle (ja-JP / es-ES / zh-CN) | bundle | authored leaves | candidates, before | contradicting, before | widening checks, before | **re-translated** | candidates, after | contradicting after | widening after | |---|---|---|---|---|---|---|---|---| | plugin-webhooks | 41 / 40 / 40 | 3 / 3 / 3 | 2 / 2 / 2 | 0 / 0 / 0 | **2 / 2 / 2** | 1 / 1 / 1 | 0 | 0 | | plugin-audit | 102 / 100 / 102 | 3 / 3 / 3 | 2 / 2 / 2 | 1 / 1 / 1 (`environment_id.help`) | **3 / 3 / 3** | 1 / 1 / 1 | 0 | 0 | | plugin-approvals | 113 / 112 / 134 | 17 / 17 / 16 | 0 / 0 / 0 | 0 / 0 / 0 | **0** | 17 / 17 / 16 | 0 | 0 | | plugin-security | 179 / 179 / 179 | 7 / 5 / 7 | 1 / 0 / 1 | 0 / 1 / 0 (es-ES `sys_position.description`) | **1 / 1 / 1** | 6 / 5 / 6 | 0 | 0 | The before candidate counts equal the objectstack-ai#20539 dev's raw counts exactly (A2). After = at `5444bb130`, both instruments: each after-candidate set is exactly the before set minus the re-translated paths (0 added), the echo counts are unchanged (no new value equals its `en` leaf), and the term scan and renamed-key check return 0. The penultimate-edit flags after are the before set minus es-ES `sys_position.description`, plus the 14 re-translated leaves that were candidates (their last edit is now this PR, after `en` moved), which is the expected shape. ### The judgment rule As in PR objectstack-ai#20652. A leaf **contradicts** the current `en` when a reader who acts on it would believe something about the current field or object that `en` now says is false: `en` now denies what the leaf asserts; the object was re-modelled, so the leaf describes a different thing; or the leaf names the record's entity by a term a rename retired from it. Added detail, narrowing, rewording, punctuation and title-casing are not contradictions. ## Re-translated (18): before and after, with the `en` each now matches (ja-JP / es-ES / zh-CN, in each bundle's existing terms: 署名シークレット / カスタムヘッダー / アウトボックス / ケイパビリティ; secreto de firma / cabeceras personalizadas / outbox / capacidades / entorno; 签名密钥 / 自定义请求头 / 发件箱 / 授权能力 / 环境.) **`plugin-webhooks` · `sys_webhook.fields.definition_json.help`**: said the JSON carries the full headers / auth / retry / payload config. `en` says the opposite, on a security field. `en`: 「Serialised Webhook JSON (see @objectstack/spec/automation/webhook) — timeout and the rest of the authored envelope. Credentials are NOT stored here: the signing secret lives in the encrypted `signing_secret` field and the custom headers in the encrypted `headers_secret` field.」 - ja-JP: 「シリアライズされた Webhook JSON(@objectstack/spec/automation/webhook 参照)— ヘッダー/認証/リトライ/ペイロード設定を含む」 → 「シリアライズされた Webhook JSON(@objectstack/spec/automation/webhook 参照)— タイムアウトなど、作成されたエンベロープの残りの設定。認証情報はここには保存されません。署名シークレットは暗号化された `signing_secret` フィールドに、カスタムヘッダーは暗号化された `headers_secret` フィールドに保存されます。」 - es-ES: 「JSON serializado de Webhook (consulte @objectstack/spec/automation/webhook): configuración completa de cabeceras/auth/reintentos/payload.」 → 「JSON serializado de Webhook (consulte @objectstack/spec/automation/webhook): el tiempo de espera y el resto del envelope definido. Las credenciales NO se almacenan aquí: el secreto de firma se guarda en el campo cifrado `signing_secret` y las cabeceras personalizadas, en el campo cifrado `headers_secret`.」 - zh-CN: 「序列化的 Webhook JSON(参见 @objectstack/spec/automation/webhook)——包含完整的 headers/auth/retry/payload 配置」 → 「序列化的 Webhook JSON(参见 @objectstack/spec/automation/webhook)——超时及所编写信封的其余配置。凭据不存储在此处:签名密钥保存在加密的 `signing_secret` 字段中,自定义请求头保存在加密的 `headers_secret` 字段中。」 **`plugin-webhooks` · `sys_webhook.description`**: said an HTTP connector plugin executes the webhook. `69f1dfd5c` replaced that executor in the source with the webhook auto-enqueuer and the shared HTTP outbox (`service-messaging`), and no HTTP connector plugin exists in the tree (`packages/plugins`, `packages/services`). The leaf also omitted the `defineStack({ webhooks })` door, which is additive. `en`: `Outbound HTTP webhook subscription. Declared in code via defineStack({ webhooks }) / defineWebhook() (materialized into rows on boot) or authored directly in the Studio editor; dispatched by the webhook auto-enqueuer onto the shared HTTP outbox.` - ja-JP: 「送信 HTTP Webhook サブスクリプション。defineWebhook() またはスタジオエディタで作成し、HTTP コネクタプラグインが実行します。」 → 「送信 HTTP Webhook サブスクリプション。コードでは defineStack({ webhooks }) / defineWebhook() で宣言する(起動時に行として実体化)か、スタジオエディタで直接作成します。Webhook 自動エンキューアが共有 HTTP アウトボックスへディスパッチします。」 - es-ES: 「Suscripción saliente de Webhook HTTP. Se crea mediante defineWebhook() en código o con el editor de Studio; la ejecuta el plugin del conector HTTP.」 → 「Suscripción saliente de Webhook HTTP. Se declara en código mediante defineStack({ webhooks }) / defineWebhook() (materializada en filas al arrancar) o se crea directamente con el editor de Studio; el encolador automático de webhooks la despacha al outbox HTTP compartido.」 - zh-CN: 「外发 HTTP Webhook 订阅。可在代码中通过 defineWebhook() 编写,或在 Studio 编辑器中维护;由 HTTP 连接器插件执行。」 → 「外发 HTTP Webhook 订阅。可在代码中通过 defineStack({ webhooks }) / defineWebhook() 声明(启动时物化为数据行),或直接在 Studio 编辑器中编写;由 Webhook 自动入队器分发到共享的 HTTP 发件箱。」 **`plugin-audit` · `sys_activity.fields.environment_id.label`**: the v5.0 rename project to environment ships no alias. `en`: `Environment` - ja-JP: 「プロジェクト」 → 「環境」 - es-ES: 「Proyecto」 → 「Entorno」 - zh-CN: 「项目」 → 「环境」 **`plugin-audit` · `sys_activity.fields.environment_id.help`**: same rename; found by the term scan and the renamed-key check, invisible to the since-last-edit instrument. `en`: `Environment context (multi-environment deployments)` - ja-JP: 「プロジェクトコンテキスト(マルチプロジェクトデプロイメント)」 → 「環境コンテキスト(マルチ環境デプロイメント)」 - es-ES: 「Contexto del proyecto (implementaciones multiproyecto).」 → 「Contexto del entorno (implementaciones multientorno).」 - zh-CN: 「项目上下文(多项目部署)」 → 「环境上下文(多环境部署)」 **`plugin-audit` · `sys_audit_log.fields.user_id.label`**: `7fe7e85d6` gave `sys_audit_log` its own `actor` principal field (label Actor; ADR-0014 D2) and relabelled the strict `sys_user` lookup `user_id` from Actor to User, because a service-token action leaves `user_id` null and records the principal in `actor`. The leaves still called `user_id` the actor: es-ES showed two fields both labelled 「Actor」, and ja-JP (操作者 beside 実行者) and zh-CN (执行人 beside 操作者) showed two synonyms. `en`: `User` - ja-JP: 「操作者」 → 「ユーザー」 - es-ES: 「Actor」 → 「Usuario」 - zh-CN: 「执行人」 → 「用户」 **`plugin-security` · `sys_position.description`**: the ADR-0090 P1 commit (`6d83431cf`) changed `en` from "Role definitions for RBAC access control" to capability distribution; the translations were find-replaced role to position and kept "for RBAC access control" (ADR-0090: capability = `permission_set`, distribution = `position`, and the word role is retired from UI copy). `en`: `Position definitions for capability distribution (ADR-0090)` - ja-JP: 「RBAC アクセス制御のためのポジション定義」 → 「ケイパビリティ配分のためのポジション定義(ADR-0090)」 - es-ES: 「Definiciones de puesto para el control de acceso RBAC」 → 「Definiciones de puesto para la distribución de capacidades (ADR-0090)」 - zh-CN: 「用于 RBAC 访问控制的岗位定义」 → 「用于分发授权能力的岗位定义(ADR-0090)」 The two closest calls on this side are `sys_webhook.description` and `sys_audit_log.fields.user_id.label`; the reasons are above. ## Stale but not contradicting (listed, left as written) | bundle · path | locales | what `en` did | |---|---|---| | webhooks · `sys_webhook.fields.triggers.help` | all three | Condensed to "(bulk_* deliver a count, not a record)" when the `en` bundle began tracking its source (`8af76aebf`). The leaf's longer "bulk_update / bulk_delete fire on predicate writes and deliver a count" is still true (the field's own source comment says the same). | | audit · `sys_audit_log.fields.user_id.help` | all three | Widened "null for system actions" to "null for non-user / service actions — see actor". The leaf is narrower, not false. | | approvals · `sys_approval_request.fields.status.options.*` (5), `sys_approval_action.fields.action.options.*` (12) | 17 / 17 / 16 | Moved from the machine value to a title (`pending` to `Pending`, `request_info` to `Request Info`, `ooo_substitute` to `Out-of-Office Substitution`). Every leaf already carried the meaning. Triage: not a contradiction. | | security · `sys_position.fields.managed_by.help`, `sys_capability.fields.managed_by.help`, `sys_permission_set.fields.managed_by.help` | all three | Added the unified tri-state wording and the legacy aliases, or reworded. The leaves' platform / package / admin reading is still true. | | security · `sys_capability.description` | all three | Added the ADR-0066 citation and named the two referencing keys. | | security · `sys_position._actions.deactivate_position.confirmText` | ja-JP, zh-CN (es-ES via the penultimate check) | Dropped "with the position". | | security · `sys_permission_set.fields.name.help` | all three | Added that the name is the set's metadata identity and cannot be renamed (clone instead). The leaf does not claim it can. This is the closest call among the leaves left alone. | ## Dispatch hypotheses, measured - **A1 held.** The ported instrument flags the known positive in all three locales, and its counts at `6bff748bb` are 3/3/3, 3/3/3, 17/17/16 and 7/5/7. - **A2 held as a lead.** The raw counts reproduce exactly. Meaning judged per candidate: 5 of the 30 / 28 / 29 candidates contradict in ja-JP / zh-CN, 4 in es-ES, plus one term-scan leaf per locale and one penultimate-edit leaf in es-ES. The named likely positive (`sys_activity.fields.environment_id.label`) is a positive in all three locales (es-ES 「Proyecto」 too). - **A3 held: values only.** `node scripts/check-i18n-bundles.mjs --write --filter=plugin-NAME` for webhooks, audit, security and approvals printed `regenerated` for each; the files were rewritten on disk and `git status --porcelain` stayed empty, so the tooling owes no companion or `en` change. Control (a commit-first ablation through `scripts/ablation-replace.mjs`): with ja-JP `sys_webhook.fields.definition_json.help` set to the `en` string, `check-i18n-bundles --filter=plugin-webhooks` goes red, `plugins/plugin-webhooks DRIFTED (1)`; the tool restored it (blob `88583c2ea5f9` == HEAD, `git diff HEAD` empty). - **A4 held.** Each changed plugin ships the new values in `dist` (built at `5444bb130`). `plugin-webhooks`: the built chunk `dist/translations-KQ72WOMS.js`, the module the plugin's `kernel:ready` hook imports, was imported and its served value (after `withSourceFallback`) read at each path: 6 of 6 equal the HEAD source and differ from the base, and 3 unchanged-leaf controls equal. `plugin-audit` and `plugin-security` inline their bundles: each locale's object literal was cut out of `dist/index.mjs` and `dist/index.js` and read by path: 24 of 24 rows equal the HEAD source and differ from the base, and 12 controls equal. A plain byte grep was not a usable reading here: esbuild escapes non-ASCII text, and the old short labels are shared by other fields. ## Verification (all at `5444bb130`, after merging `origin/main` at `9a4b2bb38`) The merge brought PR objectstack-ai#20658, a comment-only edit that includes `plugin-security/src/translations/index.ts`, a file this PR does not edit. - Build: `turbo run build --filter=@objectstack/cli... --filter=@objectstack/plugin-webhooks... --filter=@objectstack/plugin-audit... --filter=@objectstack/plugin-security...`, 59/59 tasks, `VERDICT command-exit 0`. - `pnpm --filter` test, for the three changed plugins: plugin-webhooks 13 files / 160 tests passed; plugin-audit 25 files / 363 tests passed; plugin-security 147 files / 3202 passed, 23 skipped (the translation tests `bundle-ownership.test.ts` and `position-rename-consistency.test.ts` included). `typecheck` for the three: exit 0, each `check:test-typecheck: OK`. - Gates: `node scripts/pm/dispatch-gates.mjs --commands` (no paths) derived 57 commands against the real diff; the dispatch-time list had 56, and the one added is `pnpm check:logger-receiver-detach`. All 57 exit 0. `--ran` printed "57 derived famil(ies) accounted for — 57 run, 0 NOT-MEASURED". `pnpm check:dual-build-cjs-loads` first refused with exit 3 (nine packages outside this diff had no `dist/`). Those were built and the gate re-ran: exit 0. - The four roster families printed outside the runnable list: `node scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm check:error-code-casing`, `pnpm check:filter-alias-parity`, all exit 0. - Named in the verdicts: `check:i18n` "OK (9 package(s) — all bundles in sync, no undeclared authoring keys)"; `check:i18n-stale-fill` "OK (10 bundle set(s) — no new stale fills, 0 baselined)"; `check:nul-bytes` OK. - Lint, narrowed to the nine edited bundles: `eslint --no-inline-config --format json` read 9 files, 0 errors, 0 warnings. `ESLint#isPathIgnored` answers `false` for all nine, read from the repo's own config. The config enables no type-aware linting (no `parserOptions.project`; `eslint.config.mjs` states it at lines 327-328), so a change to string values in these files cannot move the verdict on any other file. The full `pnpm lint` is left to CI. ## Acceptance notes - `position-rename-consistency.test.ts` matches the retired term as `\brole?s?\b` / `\brol(es)?\b` / 角色 / ロール. None of those matches the acronym RBAC, which is how `sys_position.description` kept "for RBAC access control" in all three locales under a green ADR-0090 guard. This PR fixes the values; the test's blind spot remains. It is outside this card's file surface and is not changed here. - The since-last-edit instrument cannot see a leaf whose KEY was renamed with its value carried (here `project_id` to `environment_id`). The renamed-key check above covers that shape. objectstack-ai#20666 runs the same instrument on the metadata-forms bundles and may want the same check. - Instrument sources and outputs were run from the session scratchpad; the widened instrument is the condensed one plus the four checks described above. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #20596
Clause-②: no
What changed
This is the fourth stage of the
domain:serviceslane of the dead-citation sweep. It coverspackages/plugins/plugin-security/src/**and nothing else. By census it is the largest package in the lane; it waited while its own fixes were in flight, and the claim (5890784382) records that they have all landed. Later stages cover the other packages, so this PR saysPart ofand 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 #19123), by the method of stages 1 to 3 (PR #20609 as
422db788a, PR #20626 asb80ab579d, PR #20634 as4d04b6be3). That is 266 sites on 258 lines in 51 files, covering 40 numbers:#8919-era) and two slash-joined second numbers (#6483/#6608,#11184/#11343), see Acceptance notes.Each rewritten line now cites the record in this repository that decided what the line describes, and says in its own words what was decided. Two numbers have an in-repo decision record, and it is preferred:
#11082cites ADR-0055's amendment (2026-09-07, transitive chains compose), and#6609cites ADR-0094 D5-R, which records that conflict ruling (option A, accept the tightening). Every other number cites the commit inorigin/mainhistory that decided it: 36 distinct shas. Three pairs share one anchor because one number was the pull request that settled the other (#6483and#6608,#16607and#16722,#16608and#16805);#12143was itself a pull request, and its squash commitf64668d3cis also where route A (#11374) reached this plugin's key columns. No number was dropped.Only comments changed. Every touched source file keeps its line count (266 lines out, 266 in, over 51 files), so no line citation into these files moves. 8 of those 266 lines hold no dead citation; they are reflow, listed under Wordings below. 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 (the gate's own
extractCitationsover the diff: 277 citations removed, 14 added, all 14 kept resolving numbers on the lines they already stood on), and no number is new to the diff. No PR number stands on an added line.Sixty-five dead sites are left on purpose: 60 test strings, 2 operator log strings and 3 generated headers (see the list below).
One more file: a
patchchangeset for@objectstack/plugin-security, because the rewritten docblocks ship (see Changeset below).Census:
plugin-security, before and afterInstrument (A1). The gate's own
node scripts/check-issue-citations.mjs --census --json, read-only and unchanged. The count below is itsallocated-but-absentfindings underpackages/plugins/plugin-security/. 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.allocated-but-absentcd901d7a5, run 2026-09-29T13:00:53Z to 13:04:37Zaa067dad3, run 13:29:02Z to 13:32:37ZThe before count matches the 143 that census
5884031174read atf11b5f20. The 3 left are the generated#11671headers. The whole-repo drop is 140, exactly this diff's census sites. Theresolvestally is 32,878 in both runs, andresolves-as-pull-request(1,984) andcross-repo-unjudged(995) did not move either. The after run was taken onaa067dad3; the headf90c9b123adds only the changeset. 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 test comments. So a second reading runs the gate's own exported
extractCitations(whole-file and comment-prose projections) andclassifyCitationover every.tsfile underplugin-security/src(216 files). It uses one board, enumerated by the gate's ownenumerateBoardat 13:08:21Z (185 pages, frontier #20647, equal to the newest).cd901d7a5aa067dad3Its src-comment column equals the census's 143, which is the control on the second instrument. The 2,307 resolving, 88 pull-request and 24 cross-repo citations are the same in both readings. A third, raw reading (every
#followed by digits, judged against the same board, whatever surrounds it) finds 331 dead occurrences before and 65 after: the 4 it sees beyond the gate are the three prose sites above and one more second number inside a kept test title.Per-number table
Sites and files count every dead occurrence in scope at the base (comments and strings, tests included, gate-invisible spellings included).
rewritten / leftcounts the sites rewritten and the sites left. Each anchor was read in its message and diff, not only its subject.#62068e13ca876: share-link enforcement takes the whole authz envelope (option-A ruling); it adds this package'sgroup-posture repro. Stage 2's anchor#6216f586f1a89: oneExecutionContextassembler, with the closed-field-set pin. The anchor the spec, runtime and rest stages gave it#6483ee58392e1: ADR-0005's allow-list enforced, nine unapproved types (permissionamong them) rolled back toallowOrgOverride: false. Its message records the zero-row measurement, theallowRuntimeCreateboundary and this suite's stub blind spot. The spec stages' anchor#656454299caad: the per-rowISharingServicewrite verdict becomes tri-state (allow / abstain / deny);#6564was that pull request#6608ee58392e1:#6608was the pull request itself; this is its squash commit#6609#8692712e185db: the 2026-08-15 ruling, option A: the seed insert stampsmanaged_by: 'platform'explicitly, forward only, and the resync skip warn stops claiming intent#871442b05af89: explain reports a deactivated permission set or position through the shared held-state vocabulary. The anchor the spec stage gave it#87576feac910b: the 2026-08-15 ruling: the master gate is the sole row-write authority for acontrolled_by_parentdetail; delegated writes keep both floors#87728abada3ba: the freeze note, Direction 4 of the 2026-08-16 master-reference ruling; it names the two ramp legs and the three shapes this guard alone refuses#87787901b2dd2: option A, a stamp-only, read-neutraltenancy.organizationField. The spec stage's anchor#8804db923a3a8:#8804was the measurement pull request: a seeder-created row is stored'admin', and resync reports resynced 0 / resyncSkipped 8#8839c25b2d52a: the 2026-08-15 ruling, reading 1: one per-objectsys_commentdelete policy, so moderation stops being dead behind the floor#8865498f4e884: the 2026-08-15 ruling, direction 1: leg 1 of the master gate drops the platform ownership floor on a sharingallow#8919b5378550e:/metapublish and rollback gated onmanage_metadata; it created the write-door census whose count rule the line applies. The rest stage's anchor#11082controlled_by_parentcomposes across a chain, bounded, failing closed. One implementation-only line (the factory split) cites61713314e, the commit that landed it#11343c0714eb5d: walled elevation requires a VERIFIED owner-email match, and the bootstrap replays on the verifyingsys_userupdate. Stage 3's anchor#113743954fb7df: route A, the 2026-08-24 ruling to declare a sourcedmaxLengthon every keyed text column; the object-file line citesf64668d3c, which applied it to this plugin's key columns#11451c33f18592: the curated half's existence read becomes one batched$incarrying the#8470predicate; the reconcile is equality-gated; the derived half's batching is filed, not decided#11518e1d773eb7: the unscoped existence page cap is measured, not trusted: one row more than the budget, and an overflowing page degrades loudly to the per-item read#115201a6855226: the derived half is batched too, unnarrowed, on its own index; a derived name whose read cannot answer is declined#1167109b4f4e4e: generated translation leaves record the source revision they were filled from. Stages 1 and 2's anchor#11702#117035cb62d88b:clone_permission_setcarries all five copied facets; the params list is the payload. The runtime stage's anchor#117251e79aa4f8: the probe of the trash and restore door, which pinned its unreachability and measured the residual#117530e4e51b0a:ActionParamSchema.carryOver, the carry-over ruling's schema half. The spec stage's anchor#118435619aace3: the 2026-08-25 ruling, option B: the packaged-permission-set lock registered at the metadata door. The verbatim quotation 「11843 同意」 is kept as written#120209cfc1f7e9: the lock extended to the restore leg, refusing on the durability channel; the residual tripwire inverted in the same change#12143f64668d3c:#12143was the pull request itself: each plugin's keyed-text-bounds pin reads the widths off its own registration path#121443a04b0125: the shared identifier schemas pinned to the storage columns that bound them; the ceiling is storage-owned#12147945e91a13: the class-level keyed-text-bounds gate over every*.object.ts, superseding the per-package pins#13176a68c61267: this package's test files put in front of tsc through the siblingtsconfig.test.json#144843f64fe6c6:organization_idstamped on everysys_record_sharewrite, with the backfill and the tenancy-ledger admission; it adds this test file. Stage 2's anchor#16518470746ae4:current_user.accessible_org_idsresolved into the RLS variable bag#166071d73d45c1: RLS membership staged on the writecheckpath, so a membership-keyed check resolves on a bare insert#16608a016f08b8: the insert-side RLScheckjudges the row that will be stored, afterbeforeInsert#166829b9581b11: thesingle-posture promotion target is chosen, not sampled: the order stated to the driver, the declared owner preferred and required verified, bounded pages with a loud ceiling#167221d73d45c1:#16722was the pull request itself#16805a016f08b8:#16805was the pull request itself; its message records the contract review's findings#168611c83ca226: thealready_have_adminguard stops letting the org-admin row count decide: two ordered, bounded legs that warn with the number examined#193078f6d83147: the duplicate-name refusal onsys_permission_setcarriesUNIQUE_VIOLATION, and the packaged-set lock answers first. The spec stage's anchorEvery cited sha matches exactly one commit (
git rev-parse --disambiguate, count 1 for each of the 36), and every one is an ancestor of the base (merge-base --is-ancestor, exit 0 for all 36; the history is complete,--is-shallow-repositoryfalse, 15,092 commits). Where an earlier stage already anchored a number, this stage reuses that anchor after checking it against this package's lines.Wordings to check
#11082: ADR-0055's only amendment (2026-09-07) is the in-repo record of the chain decision, so the tags read[ADR-0055 amendment];security-plugin.ts:8027(the thrower split into a factory) is an implementation detail the ADR does not record, so it cites61713314e.#6609: the lines already named ADR-0094 D5-R, and now say it records ruling A (permission-set-projection.ts:32,:535,permission-set-projection.test.ts:498).errors.ts:184-185. The line said the publish-time lint was 「open and unruled」. The ruling of 2026-08-16 made that false;8abada3bacorrected the sibling paragraph insecurity-plugin.tsand missed this one. It now says the ruling (commit8abada3ba) orders the lint ramp and that the ramp has not landed, which matches thesecurity-plugin.tsparagraph (1 reflow line).permission-set-projection.test.ts:14-16. The lines quoted the body of the pull request, which answers 404. They now state whatee58392e1's own message records about the same blind spot: this suite stubssaveMetaItem, and the real gate is pinned by the dogfood cases and a dedicated 403 suite (2 reflow lines).packaged-permission-set-restore-leg.test.ts:47. 「recorded on Extend the packaged-permission-set lock to therestoreleg of the write-through — the one write point it does not guard #12020's PR」 became 「was measured for commit 9cfc1f7」; the line itself already states the measurement.bootstrap-system-capabilities.test.ts:1148. 「this file's own The REST/metapublish and rollback doors carry nomanage_metadatagate, so the authoring capability the PUT/DELETE doors enforce is reachable around #8919-era rule」: the count rule it applies lives in the write-door census thatb5378550ecreated (the rule's text isbb920ee08's), not in this file. The line now says so.comment 5306089973(security-plugin.ts:8093) andcomment 5587754690(bootstrap-platform-admin-walled-owner.test.ts:482). The verbatim maintainer quotation under the second is untouched.bootstrap-platform-admin.ts:630(「a pre-ruling install」, anchor on:628),bootstrap-platform-admin-walled-owner.test.ts:493(「the TRIAGE seat's」, anchors on:463and:482),security-plugin.ts:8137(「that ruling」, anchor on:8132),identifier-storage-ceiling-pin.test.ts:51(「the triage fence at the top of this file」, anchors on:13and:25),packaged-permission-set-lock.test.ts:94(anchor on:95).bootstrap-platform-admin.ts:267-268,errors.ts:185,identifier-storage-ceiling-pin.test.ts:26(「dispatch」 became 「scope」, because the dispatch was the card's),packaged-permission-set-lock.test.ts:95,permission-set-projection.test.ts:15-16,security-plugin.ts:8094.security-plugin.ts:3078andbootstrap-platform-admin.ts:715,:1110,:1149gave up as many trailing rule characters as the anchor added, keeping at least one.The 65 sites left
describe/ittitles, which are string tokens, left as stages 1 to 3 left theirs:bootstrap-declared-capabilities.test.ts:454;bootstrap-platform-admin-existing-holder-scan.test.ts:297;bootstrap-platform-admin-promotion-selection.test.ts:281;bootstrap-platform-admin-seeded-provenance.test.ts:184;bootstrap-platform-admin-walled-owner.test.ts:504,:569,:587;bootstrap-seed-round-trips.test.ts:795(two numbers),:980;bootstrap-system-capabilities.test.ts:968,:1096;controlled-by-parent-chain.test.ts:460,:540,:578;controlled-by-parent-detail-write-authority.test.ts:594,:650,:669,:708,:724,:813;explain-engine.test.ts:171,:203,:853,:873,:893;identifier-storage-ceiling-pin.test.ts:125,:143,:160;insert-check-post-image.test.ts:573,:612,:662,:775,:838,:875,:939;objects/default-permission-sets.test.ts:299;packaged-permission-set-lock-gate.test.ts:183;packaged-permission-set-lock.test.ts:647,:812,:813;packaged-permission-set-restore-leg.test.ts:264,:265;permission-set-duplicate-name-refusal.test.ts:195;plugin-keyed-text-bounds.test.ts:67;record-share-tenant-wall.test.ts:149;rls-accessible-org-ids-plumbing.test.ts:191,:256,:325,:382;rls-check-membership-staging.test.ts:388,:400,:439,:505;security-plugin.test.ts:153;share-link-tenant-wall.test.ts:239;tenant-layer.test.ts:237.expect:identifier-storage-ceiling-pin.test.ts:172,:193(#12144) andpackaged-permission-set-lock.test.ts:694(#11703).security-plugin.ts:7864and:7872, the twologger.errorlines of the chain guards (#11082). Runtime strings are form D, and the shrink-onlydoc-authoring-prose-idbaseline already holds both (security-plugin.ts,#11082: 2).translations/{es-ES,ja-JP,zh-CN}.source-hashes.generated.ts:8(#11671). Their producer is a string literal inpackages/cli, outside this lane; the pointer is on dead tracker citations in thedomain:clipackages (689 sites, 166 numbers, 95 files): the ruling C+D stage for this lane (from #20556) #20594.#.Mechanical guard: no code token moves
The guard compares the TypeScript parser's leaf nodes, with comments as trivia and JSDoc nodes excluded, base
cd901d7a5against head. Template literals are therefore read in context. It ran over all 51 touched.tsfiles.seed-name-lookup.ts(TWO events, ONE consequencetoTWO events, ONE result): 0 files changed, as expected (exit 0).seed-name-lookup.ts(an extra key in the batched read'swhere): DIFFER (exit 1).bootstrap-system-capabilities.test.ts:1096): DIFFER (exit 1).Every mutation went through
scripts/ablation-replace.mjs, and each landed (anchor 1 to 0, blob changed). Each restore was proven byte-identical to the HEAD blob (36e6be731e6a,e1f66feaa6fc), withgit diff HEADempty and a clean tree afterwards.Changeset
This change ships bytes, so a
patchchangeset for@objectstack/plugin-security(.changeset/20596-plugin-security-provenance-anchors.md) is included. It says only that the provenance comments were re-anchored.Measured on the built package (A3):
files[]isdist,README.mdandCHANGELOG.md. After the build, the rewritten comments reachdist:ADR-0055 amendmentappears 4 times in each ofdist/index.d.ts,index.d.mts,index.jsandindex.mjs;6feac910b,498f4e884twice in each of the four;c0714eb5d,e1d773eb7,db923a3a8,470746ae4,1d73d45c1,9b9581b11once in each of the four;ee58392e13 times and1c83ca226twice in each declaration file;5cb62d88b4 times andc25b2d52a3 times in each runtime file. Positive control: the unchanged line 「declared the key's SHAPE」 beside a shipped rewrite (rls-compiler.ts:88-89) is found once inindex.d.ts, beside 「Until commit 470746a nobody did」. A never-written negative phrase appears nowhere. The only dead numbers left indistare the two kept#11082log strings in the runtime files.Gates (head
f90c9b123)pnpm check:issue-citations(self-test) exits 0.node scripts/check-issue-citations.mjsexits 0: the diff-scoped run judged 9 citations across 19 files, and all 9 resolve.pnpm check:doc-authoringexits 0, with the sibling-package prose ids at their baseline and no growth.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackatf90c9b123derived 66 commands: all 57 derived at dispatch, pluscheck:duration-unit-keys,check:dispatcher-error-vocabulary,check:engine-double-contract,check:logger-receiver-detach,check:objectql-double-limit,check:query-options-erasure,check:type-check-coverage,check:type-check-debtandcheck:where-matcher. It was re-derived after a freshgit fetch(origin/mainc6b37cd08, 3 commits ahead): the same 66. Each ran with its exit code captured before any pipe, and all 66 exit 0.--ran, fed each command with its exit code, reports 66 run, 0 NOT MEASURED (a derived zero), 0 unrun, and exits 0. A fullturbo run buildof./packages/*and./packages/*/*ran first under the shared verify lock (71 of 71 tasks, exit 0), so no gate hit an unbuilt workspace.node scripts/check-changeset-fixed.mjs,pnpm check:error-code-casingandpnpm check:filter-alias-parity, each exit 0.pnpm --filter @objectstack/plugin-security test: 147 files pass, 3,202 tests pass and 23 skip. That is every test file in the package, the 32 touched ones included.pnpm --filter @objectstack/plugin-security typecheckexits 0 (tscmain,tsconfig.scripts.json, andcheck:test-typecheckat zero). The main program reads 69 non-test files; thetsconfig.test.jsonprogram reads all 216 files undersrc/, the 147 test files included, and all 51 touched files are in it (--listFiles).eslint --no-inline-config --format jsonover the 51 touched.tsfiles gives 51 files, 0 errors and 0 warnings. All 51 are in eslint's own population (isPathIgnoredis false for each).eslint.config.mjsnever enables type-aware linting (noparserOptions.project, as its own line 328 states), so a comment edit here cannot move the verdict on any untouched file. The repo-widepnpm lintis CI's run.pnpm check:nul-bytesexits 0, and a raw scan of the 52 changed files for control bytes finds none.Acceptance notes
CITATION_REopens with a lookbehind that refuses a/before the#(scripts/check-issue-citations.mjs:449), so in#A/#Bonly#Ais a citation to the diff gate and the census, dead or alive. It is the same blind-spot family as the hyphen spelling (check-issue-citations closeout (extractor spellings):CITATION_RErefuses a hyphen after the digits, so a dead#N-wordcitation (#13398-class) is invisible to the diff gate and to the census #20636). This stage rewrote the two such prose sites inplugin-security(permission-set-overlay-discard.ts:25,platform-owner-wall-bypass.ts:69) because they are the same dead numbers in the same comment prose. A raw scan ofpackages/**/src.tsfiles against the board finds 33 dead second numbers of this shape at the base and 31 at the head (one of them the kept titlebootstrap-seed-round-trips.test.ts:795), in 14 packages. The census cannot count them, so a later stage has to look for them by hand. No instrument change here.CITATION_RErefuses a hyphen after the digits, so a dead#N-wordcitation (#13398-class) is invisible to the diff gate and to the census #20636 names 1 here onmain) wasbootstrap-system-capabilities.test.ts:1148, rewritten. 9 dead#N-wordsites remain inpackages/**/srcat the head, none in this package.#11374indrivers(16),platform-objects(14) andplugin-audit(2), anchor3954fb7df(route A);#6216incore(8),mcp(1) andplugin-hono-server(1), anchorf586f1a89;#6483(8) and#6608(4) inmetadata-protocol, anchoree58392e1;#6206incore(2),plugin-approvals(3),plugin-audit(1) andservice-storage(1), anchor8e13ca876;#8778inmetadata-core,plugin-approvalsandservice-storage, anchor7901b2dd2;#16608(4) and#16805(2) inobjectql, anchora016f08b8;#11343intypes(2), anchorc0714eb5d;#8692incli(2), anchor712e185db;#12144inmetadata-protocol(1), anchor3a04b0125;#16682incore(1), anchor9b9581b11.origin/main(c6b37cd08, read at 13:54Z). None touchesplugin-securityor any of these numbers; they add three unrelated changesets and move one row ofscripts/doc-authoring-prose-id.baseline.json(apackages/lintentry), so there was no merge.Generated by Claude Code