feat(spec)!: retire the plugin-security scan-result surface — zero consumers after the PluginSecurityScanner retirement (#15932) - #19610
Conversation
ADR-0049 enforce-or-remove; maintainer ruling 2026-09-07 (director seat, decision batch #65), adopted verbatim. The second half of #14919: that change retired `PluginSecurityScanner`, whose type-only import was the family's only importer of any kind, leaving `KernelSecurityScanResult`, `KernelSecurityVulnerability`, `PluginSecurityManifest.scanResults` and `PluginQualityMetrics.securityScan` fully published with zero consumers and no `.parse`/`.safeParse` site anywhere. The two defs leave the build whole (`RETIRED_DEFS_BY_MAJOR[18]`) because nothing parses them. The authorable keys are `retiredKey()` tombstones registered in `RETIRED_KEYS_BY_MAJOR[18]` — neither carrying shape is `.strict()`, so a bare deletion would strip an authored key in silence (ADR-0104). `PluginSecurityManifest.vulnerabilities` is a forced consequence: it was the last authorable referent of `KernelSecurityVulnerability`. No D2 conversion — a plugin security manifest and a plugin registry entry are package artifacts a publisher ships, never stack collection members and never stored `sys_metadata` rows. The D3 semantic entry `plugin-security-scan-result-surface-retired` carries the judgement. The marketplace `'scanning'` status and the incident `'malware'` type are deliberately untouched: the ruling made them conditional on a producer grep of `objectstack-ai/cloud`, which is not reachable from this session. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 136 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 7efd0522b900787f168562d2ccf4c4d81194f9e5 && git checkout 7efd0522b900787f168562d2ccf4c4d81194f9e5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5c5b67fc4140f76ca3158acea9e0845d9eebfad8 3e0a06d0b521e0df9d54dd2356b860f84d9a45ab && git checkout -B drift-repro 5c5b67fc4140f76ca3158acea9e0845d9eebfad8 && git merge --no-ff 3e0a06d0b521e0df9d54dd2356b860f84d9a45ab
node scripts/docs-audit/affected-docs.mjs --json 5c5b67fc4140f76ca3158acea9e0845d9eebfad8
|
…ger serves `check:issue-citations` went red at 0a1bac8 with eight dangling sites across five files: issue 14919 at six of them and issue 8715 at two. Measured cause, from the gate's own `--probe-cause`: deleted, all eight — not transferred, not mistyped. Probed with a lit control beside each, since both dead numbers sit next to live ones: 8714 404 / 8715 404 / 8716 200, and 14918 404 / 14919 404 / 14920 200. Scattered pairs, not a contiguous band, which is what deletion-by-author looks like. Both were live references when the prose was written. ⛔ No number is guessed and none is swapped for a plausible neighbour. Each site keeps its number in prose and now says it no longer resolves, then names a record that DOES — verified by probe, not inferred: - issue 14919 -> PR #15930, `feat(core)!: retire PluginSecurityScanner`, merged 2026-09-05, whose body opens with a closing line naming that very issue number. Probe: 200. - issue 8715 -> #11825, the half of the pair this tree cites together that still resolves, and the same whole-def disposition shape. Probe: 200. The `#` sigil is what the gate judges (`CITATION_RE`); a bare number in prose is not a citation, so the number survives verbatim and the reference stops dangling. `migrations/registry.ts` is GENERATED and was NOT hand-edited: its three copies come from the two entry files, re-emitted by `gen:migration-registry`. The module docblock feeds a reference page, so `check:generated --fix` regenerated `content/docs/references/kernel/plugin-security-advanced.mdx` through `gen:docs`. ⛔ Nothing else moves: no schema, no key, no registry entry, no changeset level, no authorable row. Comments and the prose they generate, only. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
…tes were stale
From the at-tier review, item ③.3. The POSTURE on the carve-out was right and is
kept verbatim: the marketplace 'scanning' status stays untouched and ⛔ NOT
recorded as checked. What was wrong is a COORDINATE — two of the three files
those records name no longer exist, so the present-tense clause "stay exactly as
they are, unremoved" was an assertion nobody had measured, and false for one half.
Verified here by shape rather than taken on report, since stale coordinates are
the defect being repaired. Tree entries on this branch AND on origin/main:
marketplace.zod.ts 1 ('scanning' live)
marketplace-admin.zod.ts 0
system/incident-response.zod.ts 0
'malware' in any *.zod.ts 0 (lit control: 'scanning' returns a live
declaration, so the zero is a reading)
The incident 'malware' type was a member of system/IncidentCategory, and the
whole incident-response family was retired by #15513 — maintainer ruling
2026-09-05, two days BEFORE the 2026-09-07 ruling that made it conditional.
marketplace-admin.zod was deleted outright with the cloud subpath (#16526).
⇒ The conditional question is ONE enum member wide, not three, and the records
now say so — so whoever finally runs the objectstack-ai/cloud producer grep knows
its real size. ⛔ The 'scanning' half stays genuinely unmeasured; it does not
become "checked and clean".
Also repaired in the same prose, same defect class as the previous commit: ten
citations of the deleted issue 14919 that this card's own records were handing
on. None was visible to check:issue-citations — FOLLOW-UPS.md and .changeset are
not judged surfaces, and the semantic entry's are inside string literals, which
the comment-prose projection blanks. The changeset is what an upgrading agent
greps after a tombstone error, so a dead number there is the defect the gate
exists to prevent, one surface out of its reach. Same repair form: the number is
kept in prose, said to no longer resolve, and the live record named.
⛔ Nothing about the retirement changes: no schema, key, def, registry entry,
changeset level, authorable row or generated artefact. check:generated reports
all 15 up to date.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
PluginSecurityScanner retirement (#15932)
Ready, green, reviewed — and the last step is blocked with no channelAll three landing preconditions are met and recorded on the card:
This PR has been flipped draft → ready, confirmed by ⛔ ⛔ The seat will not merge this PR, enqueue it by hand, or submit an approving review on it. The only remaining act is enabling auto-merge with Action needed from the maintainer or a seat with the channel: enable auto-merge (SQUASH). Everything else here is finished. Generated by Claude Code |
✅ Correction — this PR is NOT blocked any more. It is in the merge queue.
The maintainer re-granted the permission and the call was retried. ⛔ The earlier standing-down note on this PR — "
|
Carrier stripped on a PASS that is on record — and the seat's own enqueue error, stated plainly
⛔ What I got wrongI enqueued this PR at 00:35Z while
The guard was right and the rule it cites is one I had already read: 「双肢命中任一 ⇒ 无席内条款②复核 PASS 在案 ⛔ 禁止入队」. I ran the three landing preconditions (at-tier PASS,
✅ Why stripping now is the sanctioned act and ⛔ not a way past the checkThe guard's own text draws the line: "⛔ Stripping the label to get past this check, with no verdict on record, is the defect this leg was built from — not a way through it." There is a verdict on record, and it is cited here rather than asserted: At-tier contract review, ⇒ the condition 「PASS ⇒ 同席剥标并引记录」 is satisfied. The carrier is stripped from both carriers — this PR and its card — by the same seat that adopted the verdict, in the four-step label write, with read-back. What happens nextCarrier stripped on both sides → Generated by Claude Code |
…face (objectstack-ai#19054) (objectstack-ai#19618) Fixes objectstack-ai#19054 Clause-②: no Executes the maintainer ruling recorded verbatim on the card: 「organizationField 撤出可授权面 同意你的建议」. `object.tenancy.organizationField` leaves the authorable surface at protocol 18 (ADR-0049 enforce-or-remove). The divergence the key existed for is **not** retired — only its authorability. ## What the key was, and why it could never be more than one table's fact It answered "which column says who this platform row is ABOUT", where `tenancy.tenantField` answers "what is this object WALLED by". The spec's own docblock stated the consequence: *"For ordinary objects the two coincide and `organizationField` is never needed."* Re-measured at head before this branch: the entire repository declared it **once**, on `packages/platform-objects/src/identity/sys-api-key.object.ts` — the better-auth credential table — and zero business objects declared it. Its readers were three platform-row writers, scope-pinned by name, so an application declaration was inert by construction while still being authorable on every object. ## The shape of the change `TenancyConfigSchema` is a `strictObject`, so this is the strict-deletion route: - the key is deleted from the shape, and `TENANCY_RETIRED_KEY_GUIDANCE` gains its prescription beside the two v15.0 precedents (`tenancy.strategy`, `tenancy.crossTenantAccess`). Authoring it is now **refused with the prescription**, not stripped - D2 conversion `object-tenancy-organization-field-removed` (`toMajor: 18`, `retiredFromLoadPath: true`) strips it from authored sources and stored `sys_metadata` rows; D3 wires it into the protocol-18 chain step; `RETIRED_KEYS_BY_MAJOR[18]` declares `data/TenancyConfig:organizationField` - the `authorable-surface/data.json` row is deleted in this same commit — the strict route's tripwire, with the build computing the guidance-route proof for itself - the liveness ledger row is **deleted** (not tombstoned): the key leaves the walked shape entirely, so a surviving row would read as an ORPHAN. `liveness/README.md`'s `object` row records why, and `state-counts.md` moves `object` 51 → 50 live Limb 0 of the shared resolver now reads a platform-internal table instead of a declaration: ``` PLATFORM_STAMP_ORGANIZATION_COLUMNS = { sys_api_key: 'active_organization_id' } ``` keyed by the object's registered NAME, read by the STAMP face alone. `resolveRecordOrganizationField` and `createRecordOrganizationResolver` keep their signatures — `check:api-surface` is byte-identical — and the engine-bound face passes the name it was asked about rather than reading `objectDef.name`, because several engine doubles in this monorepo return a bare `{ tenancy, fields }` map with no `name`. ## The two facts the card said must survive 1. `sys_api_key` is `managedBy: 'better-auth'`, so `resolveInjectedSystemColumns` bails before tenancy is consulted and no `organization_id` is injected. Pinned, and the pin is now stated as the better-auth bail rather than as key-blindness (`packages/spec/src/data/injected-system-columns.test.ts`). 2. ⛔ The column is **not** renamed to `organization_id`. In this platform "has an `organization_id` column" IS the wall, so the rename would wall the credential table on an equality that excludes NULL. `plugin-security`'s Layer-0 suite pins both halves against the real shipped object. The stamp/wall divergence pin is green: `resolveRecordWallOrganizationField` never read the key and is untouched. ## Base merge after objectstack-ai#19600 landed, and the tombstone version it exposed (2026-09-23) The collision partner this section used to name, objectstack-ai#19610, has landed, and so has objectstack-ai#19600 (card objectstack-ai#15178, merged at 03:04:25Z as `d0f1845657`). objectstack-ai#19600 is one of the three PRs in the serial on `packages/spec/src/migrations/registry.ts` described in notice `5780847968`. After it landed, this PR read `dirty`.⚠️ **The sentence that stood here before was wrong in part.** `registry.ts` is only partly generated. Its `<os-generated …>` regions are regenerated. But `registry.ts:18-38` says outright that each step's `rationale` and `conversionIds` are **hand-written and merge as text**. No gate turns red when a paragraph is dropped from them. The merge was done on the branch with no rebase and no force-push. It is three commits: 1. **`bde765bf05` merges `origin/main` at `67add1301a`.** It is a merge commit with parents `7cc0ca1b3d` and `67add1301a`, and it resolved two textual conflicts by hand. - `step18.rationale` keeps objectstack-ai#19600's paragraph verbatim. Its last line is re-terminated with a trailing space, and this PR's paragraph is appended after it. - `step18.conversionIds` keeps both `'translation-per-app-settings-removed'` and `'object-tenancy-organization-field-removed'`. That gives 33 ids, 33 of them distinct. - `packages/spec/src/conversions/registry.ts` keeps both D2 conversions in `CONVERSIONS_BY_MAJOR[18]`, in landing order. The file's own rule is 「ordering within a major is application order」. - The generated regions were regenerated and never hand-merged. 2. **`9d5fb0ba5f` is regeneration only.** It regenerates the two reference pages that `os-regen-merge.sh` had deferred. 3. **`3fb1a4994c` is a CONTENT change, not merge resolution.** The merge brought in `check:future-spec-major` (objectstack-ai#19655), which landed after this PR's old base, and CI went red on two sites. Under ADR-0087 (amended 2026-09-13), a tombstone names the npm release it ships in, never the protocol major. This retirement ships `minor`, so it lands in 17.x. The commit therefore changes the prescription at `packages/spec/src/data/object.zod.ts:540` and its refusal pin at `packages/spec/src/data/object.test.ts:1947` from `@objectstack/spec 18` to `@objectstack/spec 17`. The protocol-major references (`toMajor: 18`, `RETIRED_KEYS_BY_MAJOR[18]`, `os migrate meta --from 17`) are unchanged, because the gate permits them. **Measured by the dispatching seat against the committed trees, not taken from the dev's narration:** - The merge commit against the main parent `67add1301a`: 2 files, +128/−1. Every hunk is this PR's. - The merge commit against the branch parent `7cc0ca1b3d`: 2 files, +656/−28. Every hunk is main's. - On the merged `registry.ts`, each of the following appears exactly once: - the shared closing line `dataset. '` - objectstack-ai#19600's paragraph - objectstack-ai#19600's last line, continuing with a trailing space - each of the two `conversionIds` - the retired-key row - A dark control phrase appears 0 times. - This PR's paragraph follows objectstack-ai#19600's.⚠️ **The earlier contract review (`5765681233`) names head `7cc0ca1b3d`.** Commit `3fb1a4994c` changes a string that review's AC2 pinned, so this head move is **not** regeneration-only, and the earlier record does not govern the new head. `check-clause2-carriers.mjs --pair 19618` confirms it: exit 4, C6. A fresh contract review of the current head is owed before landing. ## Verification **Re-measured at the current head `3fb1a4994c`, after the base merge.** - **CI**, measured by the seat from the head's check-runs: 35 checks, latest run per name. 33 success, 2 skipped (`Console Pin Gate`, `Packed-tarball smoke (opt-in)`), 0 failed. All five type-check lanes pass. The legacy commit status is `success`. - **Suites and gates**, from the os-dev report `5788876254`, which the seat did not re-run: | package or gate | result | | --- | --- | | `@objectstack/spec` | 516 files, 15065 passed, 1 todo | | `@objectstack/metadata-core` | 16 files, 285 passed (unchanged) | | `@objectstack/plugin-audit` | 25 files, 363 passed (unchanged) | | typecheck, spec and metadata-core | exit 0 | | `check:generated` | 15 of 15 current | | `check:future-spec-major` | exit 0 | | `dispatch-gates --ran` | 115 accounted: 112 run with exit 0, 3 NOT MEASURED | - The spec suite grew from the pre-merge 509 files / 14898 tests. The +7 files are exactly the seven spec test files `main` added in the merged range. - `check:future-spec-major` was checked against a lit control: re-planting `18` makes it exit 1 with exactly one problem. - The 3 NOT MEASURED gates were refused for build prerequisites (exit 3). They run in CI jobs that build first. The tables below are the **pre-merge** readings, kept as history: Every number in the tables below was taken at `7cc0ca1b3d`, the pre-merge head. **Reverse verification (both legs committed first, both restored byte-identically, both via `scripts/ablation-replace.mjs`):** | ablation | anchor → replacement landed | result | | --- | --- | --- | | the platform stamp row renamed (`sys_api_key` → `sys_api_key_ABLATED`) | anchor 1 → 0, blob `0be02fdcc6b7` → `21856a4a20d0` | **4 of 19** metadata-core tests RED; restore `blob == HEAD`, `git diff HEAD` empty | | the prescription's first clause replaced with placeholder text | anchor 1 → 0, blob `2e9e19825ef6` → `eea8b4c7f061` | refusal pin RED with `expected 'Unrecognized key(s) on 'tenancy': 'or…' to contain ''tenancy.organizationField' was remov…'` — the pin measures the PRESCRIPTION, not merely that parse throws; restore verified the same way | **Suites** (`pnpm test` per package, through the shared verify lock): | package | result | | --- | --- | | `@objectstack/spec` | 509 files, 14898 passed, 1 todo | | `@objectstack/metadata-core` | 16 files, 285 passed | | `@objectstack/platform-objects` | 53 files, 848 passed | | `@objectstack/plugin-security` | 117 files, 2249 passed | | `@objectstack/plugin-audit` | 25 files, 363 passed | **Typecheck:** `@objectstack/spec`, `@objectstack/metadata-core`, `@objectstack/platform-objects`, `@objectstack/plugin-audit`, `@objectstack/plugin-security` — all green, test layers included. **Gates:** `node scripts/pm/dispatch-gates.mjs --ran` reconciles **114 derived / 114 run / 0 NOT-MEASURED / 0 UNRUN** against this diff. `pnpm --filter @objectstack/spec check:generated` reports 15 of 15 artifacts current. `pnpm lint` (`eslint . --no-inline-config`, the whole repo, no narrowing) exits 0. **The three sanctioned platform-row writers' pins stayed green UNTOUCHED**, as the card required — `plugin-approvals` (`approval-node`, `backfill-platform-row-organizations`), `service-automation` (`suspended-run-store`), `service-storage` (`backfill-sys-file-organizations`): 33 + 52 + 15 tests, zero edits. The `driver-sql` and `trigger-schedule` read-neutrality suites are green untouched too (36 + 61). ## Acceptance notes **Declared widening of the dispatched file surface — three files, each because this diff makes a statement in it FALSE.** None was edited for tidiness; each is named with the measurement that forced it. 1. `packages/spec/src/shared/alias-integrity.test.ts` — RED. It pins the exact key set of the folded `tenancy` guidance table: `expected [ 'crossTenantAccess', …(2) ] to deeply equal [ 'crossTenantAccess', 'strategy' ]`. The retirement adds the third row, which is the only channel the refusal travels on. 2. `packages/plugins/plugin-security/src/tenant-layer.test.ts` — RED. It asserted the declaration off the shipped object: `expected undefined to be 'active_organization_id'`. Rewritten to pin what this suite actually owns: the stamp column exists as a field, `organization_id` does not, and `tenancy` is exactly `{ enabled: false }`. 3. `packages/plugins/plugin-audit/src/audit-writers.test.ts` — RED, two cases, and one of them is a **finding the card asked for**. See the next section. A fourth file, `packages/spec/src/automation/schedule-organization.zod.ts`, carried a docblock asserting "`tenancy.organizationField` wins there" — a statement this diff falsifies, and one that **publishes**, into `content/docs/references/automation/schedule-organization.mdx`. Corrected in prose; the generated page follows. **⭐ Finding — one sanctioned writer's pin DID have to be edited, and the reason is not cosmetic.** Two `plugin-audit` cases went red: - *"`organizationField` outranks `tenantField`"* pinned the precedence on `crm_lead`, an object declaring BOTH keys, with the comment *"No shipped object declares both; this pins the precedence so the day one does is not a coin flip."* After the retirement no application can declare a stamp column at all, so the question is **closed rather than answered**. The case is rewritten to pin the closed set — an application object carrying a lookalike column stamps from its own wall. - *"control: without the declaration the credential table still stamps the actor's org"* fed a `sys_api_key` schema with **no** `tenancy` block and pinned the actor's org, proving the stamp came from the declaration rather than from a column-name heuristic. Keying limb 0 by object name makes that shape stamp `active_organization_id` instead. This is a **real, deliberate behaviour change on a shape that is not reachable for the shipped table** — `sys_api_key` is `managedBy: 'better-auth'` and `protection: { lock: 'full' }`, so its block cannot be dropped. Recorded in the rewritten case rather than smoothed over, and the `objectstack-ai#5315` guard that did not move (column absent ⇒ fall through to the actor's org) is pinned beside it. **⭐ Finding — two issue citations this repo carries in these files do not resolve.** `check-issue-citations --base origin/main` judged 12 citations this change adds and refused all 12: `objectstack-ai#8778` and `objectstack-ai#8707` are `allocated-but-absent` (minted, ≤ frontier 19616, not on the board; deleted vs transferred NOT MEASURED). Both are pre-existing text — the diff only re-adds them by rewriting the docblocks around them. Following the gate's own prescription, the added lines now name the rulings in prose and cite the cloud record that does resolve. ⛔ No number was guessed. The standing occurrences on unchanged lines elsewhere in the tree are untouched and are not this PR's to repair. **Stale-but-green fixture residue, deliberately NOT touched** (green today, outside the dispatched surface, and not a defect — the fixtures feed drivers and engine doubles, never `TenancyConfigSchema`): `packages/drivers/driver-sql/src/sql-driver-tenant-scope.test.ts`, `packages/triggers/trigger-schedule/src/time-relative-trigger.test.ts`, `packages/plugins/plugin-approvals/src/{approval-node,backfill-platform-row-organizations}.test.ts`, `packages/services/service-automation/src/suspended-run-store.test.ts`, `packages/services/service-storage/src/backfill-sys-file-organizations.test.ts` still author `tenancy: { …, organizationField: … }` in raw object-definition fixtures. Their assertions remain true; what has gone vacuous is the *claim* that the driver / wall face is neutral about a key nobody can write. `packages/lint/src/validate-object-field-refs.ts` carries the key in a list of scalars it deliberately does not judge. **No tree-scoped absence pin is added**, and that is a decision rather than an omission: the playbook's tree-scoped form would have to declare its radius in `scripts/cross-package-test-inputs.mjs` and `turbo.json`, both far outside this card's surface, and it would go red against exactly the six inert fixtures above. The absence is instead enforced where it is cheap and exact — `authorable-surface/data.json` has no row, and `check:authorable-surface` is the gate over that baseline. ## Clause-② re-judged from the diff `no`, and the diff agrees. No hunk puts a new key on a published payload: the guidance row is a prescription string, the `RETIRED_KEYS_BY_MAJOR` / `CONVERSIONS_BY_MAJOR` entries are registry rows, `json-schema/**` **loses** a key, and `api-surface/` is byte-identical — `resolveRecordOrganizationField`'s signature is unchanged. This is a pure retirement, which narrows. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 --- _Generated by [Claude Code](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #15932
Clause-②: yes
ADR-0049 enforce-or-remove. Executes the ruling on #15932 (director seat, decision batch #65, 2026-09-07, maintainer verbatim 「同意」). This is the second half of the⚠️ Citation corrected: the number that landing was filed under — 14919, written bare on purpose — no longer resolves on the board (404 at 2026-09-21T18:3xZ; lit control: the neighbouring 14920 returns 200), so the digits are kept greppable while the sigil is dropped, which is the same treatment this repo gave the dead ⚠️ Carrier corrected: an earlier draft of this sentence called that precedent a
PluginSecurityScannerretirement, whose live record is PR #15930.[#14423]docblock citation that PR #19609 repaired inpackages/metadata/src/metadata-manager.ts.Blocked-by: #14423line. There is no such line —Blocked-by:.*14423returns 0 acrossorigin/main(lit control: realBlocked-by:lines do exist, in five files and more), and #19609's own diff removes* [#14423] The keyed plural read …from a docblock. The precedent itself is real and reads verbatim "the card it was filed under — issue 14423, written here without a leading hash because it no longer resolves"; only its carrier was misnamed. ⛔ No replacement number is guessed: #15930 is the PR whose patch did the work, ⛔ not a re-issued card. That change retiredPluginSecurityScanner, and its type-only import was the scan-result family's only importer of any kind, so the schemas it fed went from one type-only importer to zero consumers of any kind while staying fully published.What is retired
KernelSecurityScanResult(def + 3 exports)RETIRED_DEFS_BY_MAJOR[18]KernelSecurityVulnerability(def + 3 exports)RETIRED_DEFS_BY_MAJOR[18]PluginSecurityManifest.scanResultsretiredKey()tombstone,RETIRED_KEYS_BY_MAJOR[18]PluginSecurityManifest.vulnerabilitiesretiredKey()tombstone,RETIRED_KEYS_BY_MAJOR[18]— see Scope belowPluginQualityMetrics.securityScanretiredKey()tombstone,RETIRED_KEYS_BY_MAJOR[18]Two routes because the two questions have different answers. Nothing parses the two defs, so there is no author a prescription could reach and a tombstone would be noise. The three keys sit on shapes that are not
.strict(), where a bare deletion is a silent strip (ADR-0104) — so each becomes aretiredKey()tombstone, audible in both channels:tsc(input typenever) and the parse, which raises the prescription itself.No D2 conversion. A plugin security manifest and a plugin registry entry are package artifacts a publisher ships — never stack collection members, never stored
sys_metadatarows — so the conversion chain has no seam that would see one. That is the disposition the siblingkernel-plugin-security-durations-unit-in-keyentry already records for this same manifest. The D3 semantic entryplugin-security-scan-result-surface-retiredcarries the judgement.Premise, re-measured first-hand on
origin/main@236cec19a5KernelSecurityScanResult/KernelSecurityVulnerabilityinpackages/**/*.tsoutside the declaring modulePluginSecurityManifestinsideplugin-security-advanced.zod.tspackages/core/src/security/security-scanner.tsPluginSecurityScannerretirement (PR #15930) landedPluginQualityMetrics.securityScaninpackages/**/*.tsplugin-registry.test.tsonly — the spec's own self-test87af769e— does it import any of this?@objectstack/specis imported there ⇒ no sibling fix and no pin bump are owedThe authorable-row count is 27, not the 22 the card carried. Measured with the playbook's instrument on
authorable-surface/kernel.json: 8 rows forKernelSecurityScanResult, 17 forKernelSecurityVulnerability, plusPluginSecurityManifest:scanResultsandPluginQualityMetrics:securityScan— 28 counting the forced-consequencePluginSecurityManifest:vulnerabilities. The disagreement is reported, not reconciled: the 22 is superseded, and the FOLLOW-UPS row now says so.Scope — one key outside the four names, reported rather than absorbed
PluginSecurityManifest.vulnerabilitiesis not one of the four names the ruling listed. It is a forced consequence: it was an array ofKernelSecurityVulnerabilityand the last authorable referent of a def the ruling retires by name, so it cannot outlive that def, and keeping the def alive only to carry it would be keeping the retired family alive under a second name. It is not a neighbour retired by proximity — the fence's stated concern — and it is named here, in the registry entry, in the changeset and in the hand-back.⛔ The outstanding carve-out is ONE enum member wide, not three — and it is NOT recorded as checked.
objectstack-ai/cloud. Two of the three no longer exist in this tree, so only one is still outstanding:3e0a06d0b5'scanning'status,marketplace.zod.ts.zod.ts, atmarketplace.zod.ts:348— the sole outstanding carve-outmarketplace-admin.zod.ts'malware'type,incident-response.zod.ts'malware'conditional;malwarereturns 0 in any.zod.tsInstrument controls, so the two zeros are readings rather than a dead grep:
marketplace*.zod.tson the samefindreturnsmarketplace.zod.ts, andmalwareon the same grep returns 7 non-.zod.tsfiles (ADRs, design docs, records). An earlier draft of this body named all three files in the present tense; that was wrong and is retracted here.objectstack-ai/cloudis not reachable from this session, so the producer question for'scanning'is genuinely NOT MEASURED. ⛔ The absence of these names from this diff is not evidence about them.Breaking, for a population that is not measured
@objectstack/specis published, so removing six exports and three authorable keys is breaking for consumers no download, dependent or source telemetry was consulted for — exactly as that retirement's own changeset (PR #15930) says of its own three exports. That was an input to the ruling, not a reason to soften the removal. No deprecation window (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」). The release note is written centrally;content/docs/releases/is untouched.securityScan.passed === truewas gating on nothing.major, a live gate refuses itThe ruling and the dispatch both say
major.scripts/check-changeset-no-major.mjshard-refuses amajorbump for the duration of the launch window (every publishable package is in one Changesetsfixedgroup, so onemajorpromotes ~70 packages), and the retirement playbook was corrected to say so in #19446, which is onmain. Amajorchangeset here is a guaranteed-red PR that cannot land.This PR therefore ships
minor+ a BREAKING banner carrying the FROM → TO mapping and the one-line fix — the carrier the window designates for breaking-ness — plus the ADR-0087 disposition marker.check-changeset-no-major.mjsandcheck-adr-0087-registration.mjsare both green on it. This is flagged, not silently chosen: if the seat wants the literalmajor, that is a decision about the launch-window guard, not about this diff.Verification
pnpm --filter @objectstack/spec buildpnpm --filter @objectstack/spec testpnpm --filter @objectstack/spec typecheckpnpm --filter @objectstack/spec check:generated--fix, never hand-editedcheck-adr-0087-registration.mjsregistered plugin-security-scan-result-surface-retiredcheck-changeset-no-major.mjsmajorbump introducedTwo gates fired on the way, and both were answered rather than routed around. The json-schema manifest deletion gate refused the two vanished defs until their keys left
json-schema.manifest/kernel.jsonand each was declared inRETIRED_DEFS_BY_MAJOR; the authorable-surface deletion gate then refused the 25 orphaned key rows until they leftauthorable-surface/kernel.jsonin the same commit. That sequence is the removal's own evidence and is why the ratchets moved. ⛔authorable-surface.base.jsonwas not touched.Reverse verification — the refusal pin can fail. The
scanResultstombstone was ablated toz.array(z.unknown()).optional()withscripts/ablation-replace.mjs, which proved the mutation on disk (anchor 1 → 0, blob0f3af37f5068→969efcdaa46a) before running anything. Result: exactly one test failed — thescanResultsrefusal pin — and the other four passed. The restore leg verified blob == HEAD andgit diff HEADempty.Acceptance notes
Noted, not filed — observed while executing, outside this card's scope, and no in-flight PR or person is known to be heading for these files:
packages/spec/src/kernel/plugin-security.zod.tsdeclares a parallel, unprefixed scan-result family —SecurityVulnerabilitySchemaandSecurityScanResultSchema, near-duplicates of the pair retired here, with their own self-test inplugin-security.test.ts. It is outside the four names and is deliberately untouched; the pin test asserts both are still exported, so the fence is machine-checked rather than described. Whether it is live is a separate census this card did not take.plugin-security-advanced.test.ts, the declaring module's own self-test, contained zero references to the scan-result family. The premise calledplugin-security.test.tsthe family's self-test; it is in fact the other family's. The retired family had no self-test at all — a reading slightly stronger than the card's.PR body maintained by the
domain:specexecution seat, sessionsession_01UDXER3sdqfeVYpEWZs5mZx; the diff is the dev's.Generated by Claude Code