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 |
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