diff --git a/.changeset/15932-plugin-security-scan-result-surface-retired.md b/.changeset/15932-plugin-security-scan-result-surface-retired.md
new file mode 100644
index 00000000000..f3e068d3567
--- /dev/null
+++ b/.changeset/15932-plugin-security-scan-result-surface-retired.md
@@ -0,0 +1,98 @@
+---
+'@objectstack/spec': minor
+---
+
+feat(spec)!: retire the plugin-security scan-result surface — zero consumers after the scanner retirement (#15932)
+
+**BREAKING** — the plugin-security scan-result family is removed. ADR-0049
+enforce-or-remove; maintainer ruling 2026-09-07 (director seat, decision batch
+#65), adopted verbatim 「同意」.
+
+This is the second half of the scanner retirement — issue 14919, a number since
+deleted from the board, landed as PR #15930. That change retired `PluginSecurityScanner`,
+the `@objectstack/core` class that shipped as a security control and returned
+`status: "passed"` for every plugin it was ever handed. The **schemas** it fed
+survived it — and that scanner's type-only import was their only importer of any
+kind, so the family went from one type-only importer to **zero consumers** while
+staying fully published: 27 authorable rows on `authorable-surface/kernel.json`,
+six `api-surface` exports, two authorable defaults and two json-schema manifest
+keys, with no `.parse` or `.safeParse` site against either schema anywhere. An
+author could write any of it, be accepted, and get nothing. That is the
+declared-not-enforced shape, one layer out from the class removed for the same
+reason. "Declare an owner to enforce" was refused by name: it would rebuild the
+scanner just retired.
+
+### FROM → TO
+
+| removed | what to write instead |
+| --- | --- |
+| `KernelSecurityScanResult`, `KernelSecurityScanResultParsed`, `KernelSecurityScanResultSchema` (exports) | nothing — delete the import. No replacement type exists. |
+| `KernelSecurityVulnerability`, `KernelSecurityVulnerabilityParsed`, `KernelSecurityVulnerabilitySchema` (exports) | nothing — delete the import. No replacement type exists. |
+| `PluginSecurityManifest.scanResults` | delete the key |
+| `PluginSecurityManifest.vulnerabilities` | delete the key |
+| `PluginQualityMetrics.securityScan` | delete the key |
+
+**The one-line fix: delete the keys and every import of the two types.** Plugin
+security scanning is not a platform capability and there is no replacement
+schema. What the platform does still enforce is unchanged: `permissions` and
+`sandbox` on the same `PluginSecurityManifest`, and artifact provenance through
+`verifyPluginArtifactIntegrity` and the plugin signature verifier — which tell
+you an artifact is the one its publisher signed, and never that it is safe. For
+dependency vulnerabilities use the tools built for it against your own project
+(`npm audit` / `pnpm audit`, Dependabot, the GitHub Advisory Database, OSV), and
+treat an unaudited third-party plugin as untrusted code. A publisher who used
+`scanResults` to advertise diligence keeps the surviving `securityContact` and
+`vulnerabilityDisclosure` blocks, which are contact terms rather than a verdict.
+
+⚠️ Runtime behaviour is deliberately **unchanged**. Nothing ever read any of
+these keys, so deleting one removes no check that was running. A consumer that
+gated on `securityScan.passed === true` was gating on nothing — the remediation
+is to audit with a real tool, not to find a replacement key.
+
+### The retirement kit
+
+- The two **defs** leave the build whole — `RETIRED_DEFS_BY_MAJOR[18]`
+ (`kernel/KernelSecurityScanResult`, `kernel/KernelSecurityVulnerability`) —
+ because nothing parses them, so there is no author a tombstone could reach.
+- The three **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): the tombstone is
+ audible in both channels — `tsc` (input type `never`) 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 and
+ never stored `sys_metadata` rows, so the chain has no seam that would see one
+ — the disposition the sibling `kernel-plugin-security-durations-unit-in-key`
+ entry already records for this same manifest. The D3 semantic entry
+ `plugin-security-scan-result-surface-retired` carries the judgement.
+- `PluginSecurityManifest.vulnerabilities` is a **forced consequence**, not one
+ of the four names the ruling listed: it was the last authorable referent of
+ `KernelSecurityVulnerability` and could not outlive the def.
+- **No deprecation window** (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」).
+
+⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec`
+is published, so this is breaking for consumers no download, dependent or source
+telemetry was consulted for — exactly as that retirement's own changeset says of its
+three exports. That was an input to the ruling, not a reason to soften the removal.
+
+⛔ **Untouched, and not checked:** the marketplace `'scanning'` status
+(`marketplace.zod.ts`). The ruling made it conditional on a producer grep of
+`objectstack-ai/cloud`, and that repository was not reachable from the session
+that executed this card, so it stays exactly as it is and its absence from this
+diff is not evidence about it.
+
+⚠️ **The two members the ruling paired with it were ALREADY GONE** — measured on
+this tree, not assumed. 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.ts` was deleted outright with
+the cloud subpath (#16526). Both files return zero tree entries here, against a
+lit control where `'scanning'` still returns a live declaration. So the
+conditional question is **one** enum member wide, not three.
+
+`Clause-②: yes (narrowing)` — a published surface is removed: six exports leave
+the built `.d.ts` and three authorable keys stop being writable, so the accept
+set a consumer writes against narrows. Nothing is widened and nothing is
+renamed. Contract-review tier.
+
+
diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx
index a7a6966df63..47d11b4cc71 100644
--- a/content/docs/references/index.mdx
+++ b/content/docs/references/index.mdx
@@ -1,6 +1,6 @@
---
title: Protocol Reference
-description: Every schema published by @objectstack/spec — 1533 schemas across 14 protocol modules
+description: Every schema published by @objectstack/spec — 1531 schemas across 14 protocol modules
---
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -25,7 +25,7 @@ counts are sums of the rows they head. Regenerate with
| [Data Protocol](/docs/references/data) | 29 | 175 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
| [Integration Protocol](/docs/references/integration) | 1 | 24 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. |
-| [Kernel Protocol](/docs/references/kernel) | 30 | 159 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. |
+| [Kernel Protocol](/docs/references/kernel) | 30 | 157 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. |
| [Marketplace Protocol](/docs/references/marketplace) | 4 | 30 | The package & marketplace format — package identity and versions, listing, publish, review, search, install, template manifests. |
| [QA Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. |
| [Security Protocol](/docs/references/security) | 5 | 30 | Permission sets, row-level security, sharing rules, tenancy posture. |
@@ -33,7 +33,7 @@ counts are sums of the rows they head. Regenerate with
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
| [System Protocol](/docs/references/system) | 34 | 273 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
| [UI Protocol](/docs/references/ui) | 16 | 158 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
-| **Total** | **195** | **1533** | 14 protocol modules |
+| **Total** | **195** | **1531** | 14 protocol modules |
---
@@ -197,7 +197,7 @@ The single connector protocol (ADR-0097) — catalog descriptors and provider-bo
## Kernel Protocol
-**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **30 pages, 159 schemas**
+**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **30 pages, 157 schemas**
Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry.
@@ -227,7 +227,7 @@ Plugin lifecycle and manifests, capabilities and security, metadata loading, ser
| [`plugin-loading.zod.ts`](/docs/references/kernel/plugin-loading) | `PluginLoadingEvent`, `PluginLoadingState` |
| [`plugin-registry.zod.ts`](/docs/references/kernel/plugin-registry) | `PluginInstallConfig`, `PluginQualityMetrics`, `PluginRegistryEntry`, `PluginSearchFilters`, `PluginStatistics`, `PluginVendor` |
| [`plugin-security.zod.ts`](/docs/references/kernel/plugin-security) | `DependencyGraph`, `DependencyGraphNode`, `PackageDependencyConflict`, `PackageDependencyResolutionResult`, `PluginProvenance`, `PluginTrustScore`, `ResolvedPackageDependency`, `SBOM`, `SBOMEntry`, `SecurityPolicy`, `SecurityScanResult`, `SecurityVulnerability`, `VulnerabilitySeverity` |
-| [`plugin-security-advanced.zod.ts`](/docs/references/kernel/plugin-security-advanced) | `KernelSecurityPolicy`, `KernelSecurityScanResult`, `KernelSecurityVulnerability`, `PermissionAction`, `PermissionScope`, `PluginPermission`, `PluginPermissionSet`, `PluginSecurityManifest`, `PluginTrustLevel`, `ResourceType`, `RuntimeConfig`, `SandboxConfig` |
+| [`plugin-security-advanced.zod.ts`](/docs/references/kernel/plugin-security-advanced) | `KernelSecurityPolicy`, `PermissionAction`, `PermissionScope`, `PluginPermission`, `PluginPermissionSet`, `PluginSecurityManifest`, `PluginTrustLevel`, `ResourceType`, `RuntimeConfig`, `SandboxConfig` |
| [`plugin-structure.zod.ts`](/docs/references/kernel/plugin-structure) | `OpsDomainModule`, `OpsFilePath`, `OpsPluginStructure` |
| [`plugin-validator.zod.ts`](/docs/references/kernel/plugin-validator) | `PluginMetadata`, `ValidationError`, `ValidationResult`, `ValidationWarning` |
| [`plugin-versioning.zod.ts`](/docs/references/kernel/plugin-versioning) | `BreakingChange`, `CompatibilityLevel`, `CompatibilityMatrixEntry`, `DependencyConflict`, `DeprecationNotice`, `MultiVersionSupport`, `PluginCompatibilityMatrix`, `PluginDependencyResolutionResult`, `PluginVersionMetadata`, `SemanticVersion`, `VersionConstraint` |
diff --git a/content/docs/references/kernel/plugin-registry.mdx b/content/docs/references/kernel/plugin-registry.mdx
index ff0125e8b62..04b9295cc2e 100644
--- a/content/docs/references/kernel/plugin-registry.mdx
+++ b/content/docs/references/kernel/plugin-registry.mdx
@@ -51,7 +51,7 @@ const result = PluginInstallConfigSchema.parse(data);
| **testCoverage** | `number` | optional | |
| **documentationScore** | `number` | optional | |
| **codeQuality** | `number` | optional | |
-| **securityScan** | `{ lastScanDate?: string; vulnerabilities?: object; passed: boolean }` | optional | |
+| **securityScan** | `never` | optional | [REMOVED] `PluginQualityMetrics.securityScan` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the block declared a last-scan date, per-severity vulnerability counts and a `passed` verdict, and no scanner, registry, installer or UI ever produced or read one, so a plugin could publish `passed: true` with nothing at all behind it. Delete the key. There is no replacement key: plugin security scanning is not a platform capability, and the scan-result family this block mirrored was retired with it. The sibling quality metrics — `testCoverage`, `documentationScore`, `codeQuality` and `conformanceTests` — are unchanged. Audit dependencies with a tool built for it (npm audit, pnpm audit, Dependabot, the GitHub Advisory Database, OSV). |
| **conformanceTests** | `{ protocolId: string; passed: boolean; totalTests: integer; passedTests: integer; … }[]` | optional | |
### Nested Shape: `PluginQualityMetrics.conformanceTests[number]`
@@ -85,7 +85,7 @@ const result = PluginInstallConfigSchema.parse(data);
| **compatibility** | `{ minObjectStackVersion?: string; maxObjectStackVersion?: string; nodeVersion?: string; platforms?: Enum<'linux' \| 'darwin' \| 'win32' \| 'browser'>[] }` | optional | |
| **links** | `{ homepage?: string; repository?: string; documentation?: string; bugs?: string; … }` | optional | |
| **media** | `{ icon?: string; logo?: string; screenshots?: string[]; video?: string }` | optional | |
-| **quality** | `{ testCoverage?: number; documentationScore?: number; codeQuality?: number; securityScan?: object; … }` | optional | |
+| **quality** | `{ testCoverage?: number; documentationScore?: number; codeQuality?: number; conformanceTests?: object[] }` | optional | |
| **statistics** | `{ downloads: integer; downloadsLastMonth: integer; activeInstallations: integer; ratings?: object; … }` | optional | |
| **license** | `string` | optional | SPDX license identifier |
| **pricing** | `{ model: Enum<'free' \| 'freemium' \| 'paid' \| 'enterprise'>; price?: number; currency?: string; billingPeriod?: Enum<'one-time' \| 'monthly' \| 'yearly'> }` | optional | |
@@ -117,6 +117,16 @@ const result = PluginInstallConfigSchema.parse(data);
| **extensionPoints** | `{ id: string; name: string; description?: string; type: Enum<'action' \| 'hook' \| 'widget' \| 'provider' \| 'transformer' \| 'validator' \| 'decorator'>; … }[]` | optional | Points where other plugins can extend this plugin |
| **extensions** | `{ targetPluginId: string; extensionPointId: string; implementation: string; priority: integer }[]` | optional | Extensions contributed to other plugins |
+### Nested Shape: `PluginRegistryEntry.quality`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **testCoverage** | `number` | optional | |
+| **documentationScore** | `number` | optional | |
+| **codeQuality** | `number` | optional | |
+| **securityScan** | `never` | optional | [REMOVED] `PluginQualityMetrics.securityScan` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the block declared a last-scan date, per-severity vulnerability counts and a `passed` verdict, and no scanner, registry, installer or UI ever produced or read one, so a plugin could publish `passed: true` with nothing at all behind it. Delete the key. There is no replacement key: plugin security scanning is not a platform capability, and the scan-result family this block mirrored was retired with it. The sibling quality metrics — `testCoverage`, `documentationScore`, `codeQuality` and `conformanceTests` — are unchanged. Audit dependencies with a tool built for it (npm audit, pnpm audit, Dependabot, the GitHub Advisory Database, OSV). |
+| **conformanceTests** | `{ protocolId: string; passed: boolean; totalTests: integer; passedTests: integer; … }[]` | optional | |
+
---
diff --git a/content/docs/references/kernel/plugin-security-advanced.mdx b/content/docs/references/kernel/plugin-security-advanced.mdx
index a2818598dce..353fabcd3f9 100644
--- a/content/docs/references/kernel/plugin-security-advanced.mdx
+++ b/content/docs/references/kernel/plugin-security-advanced.mdx
@@ -14,9 +14,16 @@ Features:
- Fine-grained permission system
- Resource access control
- Sandboxing and isolation
-- Security scanning and verification
- Runtime security monitoring
+⛔ NOT security scanning. The scan-result family — `KernelSecurityScanResult`,
+`KernelSecurityVulnerability`, `PluginSecurityManifest.scanResults` and its
+sibling list `.vulnerabilities` — was retired under ADR-0049 enforce-or-remove
+(#15932), after the runtime scanner that was its last type-only importer went
+the same way. That scanner retirement was issue 14919 — a number DELETED from
+the board and no longer resolving; its live record is PR #15930, which landed
+the removal. Nothing on this platform scans a plugin.
+
**Source:** `packages/spec/src/kernel/plugin-security-advanced.zod.ts`
@@ -24,8 +31,8 @@ Features:
## TypeScript Usage
```typescript
-import { KernelSecurityPolicySchema, KernelSecurityScanResultSchema, KernelSecurityVulnerabilitySchema, PermissionActionSchema, PermissionScopeSchema, PluginPermissionSchema, PluginPermissionSetSchema, PluginSecurityManifestSchema, PluginTrustLevelSchema, ResourceTypeSchema, RuntimeConfigSchema, SandboxConfigSchema } from '@objectstack/spec/kernel';
-import type { KernelSecurityPolicy, KernelSecurityScanResult, KernelSecurityVulnerability, PermissionAction, PermissionScope, PluginPermission, PluginPermissionSet, PluginSecurityManifest, PluginTrustLevel, ResourceType, RuntimeConfig, SandboxConfig } from '@objectstack/spec/kernel';
+import { KernelSecurityPolicySchema, PermissionActionSchema, PermissionScopeSchema, PluginPermissionSchema, PluginPermissionSetSchema, PluginSecurityManifestSchema, PluginTrustLevelSchema, ResourceTypeSchema, RuntimeConfigSchema, SandboxConfigSchema } from '@objectstack/spec/kernel';
+import type { KernelSecurityPolicy, PermissionAction, PermissionScope, PluginPermission, PluginPermissionSet, PluginSecurityManifest, PluginTrustLevel, ResourceType, RuntimeConfig, SandboxConfig } from '@objectstack/spec/kernel';
// Validate data
const result = KernelSecurityPolicySchema.parse(data);
@@ -93,62 +100,6 @@ const result = KernelSecurityPolicySchema.parse(data);
| **retention** | `never` | optional | [REMOVED] `KernelSecurityPolicy.auditLog.retention` was renamed to `retentionDays` in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to `retentionDays`; the value (days) is unchanged. |
----
-
-## KernelSecurityScanResult
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **timestamp** | `string` | ✅ | |
-| **scanner** | `{ name: string; version: string }` | ✅ | |
-| **status** | `Enum<'passed' \| 'failed' \| 'warning'>` | ✅ | |
-| **vulnerabilities** | `{ cve?: string; id: string; severity: Enum<'critical' \| 'high' \| 'medium' \| 'low' \| 'info'>; category?: string; … }[]` | optional | |
-| **codeIssues** | `{ severity: Enum<'error' \| 'warning' \| 'info'>; type: string; file: string; line?: integer; … }[]` | optional | |
-| **dependencyVulnerabilities** | `{ package: string; version: string; vulnerability: object }[]` | optional | |
-| **licenseCompliance** | `{ status: Enum<'compliant' \| 'non-compliant' \| 'unknown'>; issues?: object[] }` | optional | |
-| **summary** | `{ totalVulnerabilities: integer; criticalCount: integer; highCount: integer; mediumCount: integer; … }` | ✅ | |
-
-### Nested Shape: `KernelSecurityScanResult.codeIssues[number]`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | |
-| **type** | `string` | ✅ | Issue type (e.g., sql-injection, xss) |
-| **file** | `string` | ✅ | |
-| **line** | `integer` | optional | |
-| **message** | `string` | ✅ | |
-| **suggestion** | `string` | optional | |
-
-
----
-
-## KernelSecurityVulnerability
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **cve** | `string` | optional | |
-| **id** | `string` | ✅ | |
-| **severity** | `Enum<'critical' \| 'high' \| 'medium' \| 'low' \| 'info'>` | ✅ | |
-| **category** | `string` | optional | |
-| **title** | `string` | ✅ | |
-| **location** | `string` | optional | |
-| **remediation** | `string` | optional | |
-| **description** | `string` | ✅ | |
-| **affectedVersions** | `string[]` | ✅ | |
-| **fixedIn** | `string[]` | optional | |
-| **cvssScore** | `number` | optional | |
-| **exploitAvailable** | `boolean` | optional (default: `false`) | |
-| **patchAvailable** | `boolean` | optional (default: `false`) | |
-| **workaround** | `string` | optional | |
-| **references** | `string[]` | optional | |
-| **discoveredDate** | `string` | optional | |
-| **publishedDate** | `string` | optional | |
-
-
---
## PermissionAction
@@ -277,8 +228,8 @@ Scope of permission application
| **permissions** | `{ permissions: object[]; groups?: object[]; defaultGrant?: Enum<'prompt' \| 'allow' \| 'deny' \| 'inherit'> }` | ✅ | |
| **sandbox** | `{ enabled?: boolean; level?: Enum<'none' \| 'minimal' \| 'standard' \| 'strict' \| 'paranoid'>; runtime?: object; filesystem?: object; … }` | ✅ | |
| **policy** | `{ csp?: object; cors?: object; rateLimit?: object; authentication?: object; … }` | optional | |
-| **scanResults** | `{ timestamp: string; scanner: object; status: Enum<'passed' \| 'failed' \| 'warning'>; vulnerabilities?: object[]; … }[]` | optional | |
-| **vulnerabilities** | `{ cve?: string; id: string; severity: Enum<'critical' \| 'high' \| 'medium' \| 'low' \| 'info'>; category?: string; … }[]` | optional | |
+| **scanResults** | `never` | optional | [REMOVED] `PluginSecurityManifest.scanResults` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing on this platform ever produced, stored or read a security scan result, so an authored array parsed cleanly and changed nothing. Delete the key. There is no replacement key: plugin security scanning is not a platform capability. What this manifest still enforces is `permissions` and `sandbox`; artifact provenance is answered by the plugin signature verifier, which tells you an artifact is the one its publisher signed and never that it is safe. Audit dependencies with a tool built for it (npm audit, pnpm audit, Dependabot, the GitHub Advisory Database, OSV). |
+| **vulnerabilities** | `never` | optional | [REMOVED] `PluginSecurityManifest.vulnerabilities` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it was an array of `KernelSecurityVulnerability`, the other half of the scan-result family, and leaves with it: nothing ever wrote the list and nothing ever read it, so declaring a known vulnerability against a plugin warned nobody and blocked no install. Delete the key. There is no replacement key: plugin security scanning is not a platform capability. Publish vulnerability contact and disclosure terms through the surviving `securityContact` and `vulnerabilityDisclosure` blocks on this same manifest, and audit dependencies with a tool built for it (npm audit, pnpm audit, Dependabot, the GitHub Advisory Database, OSV). |
| **codeSigning** | `{ signed: boolean; signature?: string; certificate?: string; algorithm?: string; … }` | optional | |
| **certifications** | `{ name: string; issuer: string; issuedDate: string; expiryDate?: string; … }[]` | optional | |
| **securityContact** | `{ email?: string; url?: string; pgpKey?: string }` | optional | |
diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
index f00b4554d0b..f1e5d637489 100644
--- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
+++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
@@ -260,7 +260,7 @@ directory rather than per file.
| `api/` | 451 |
| `identity/` | 32 |
| `integration/` | 8 |
-| `kernel/` | 257 |
+| `kernel/` | 247 |
| `marketplace/` | 29 |
| `qa/` | 6 |
| `shared/` | 20 |
diff --git a/docs/qa/platform-checklist/FOLLOW-UPS.md b/docs/qa/platform-checklist/FOLLOW-UPS.md
index 42b08ba8f63..27246027d7c 100644
--- a/docs/qa/platform-checklist/FOLLOW-UPS.md
+++ b/docs/qa/platform-checklist/FOLLOW-UPS.md
@@ -183,12 +183,38 @@ governance hole.
| surface | evidence | the deadness, precisely |
|---|---|---|
-| `KernelSecurityScanResult` / `KernelSecurityVulnerability` / `PluginSecurityManifest.scanResults` (`packages/spec/src/kernel/plugin-security-advanced.zod.ts,476,625`) | no `.parse`/`.safeParse` site anywhere; **zero** consumers of any kind since #14919 retired the dead scanner that was the last type-only importer | 22 rows published to `packages/spec/authorable-surface/kernel.json` with zero authors and zero parsers. The whole `plugin-security-advanced` module has no runtime consumer. |
-| `PluginQualityMetrics.securityScan` (`packages/spec/src/kernel/plugin-registry.zod.ts`) | spec self-test only | Nothing reads or writes it at runtime. |
-| Marketplace/incident scan vocab (`marketplace.zod.ts` 'scanning' status, `marketplace-admin.zod.ts,193`, `incident-response.zod.ts` 'malware') | declared-only enum members, no producer in this repo | Cloud/EE surface. Same shape as the `'failed'`/`'expired'` upload statuses #7667 had to close: declared, published, no writer. |
+| ~~`KernelSecurityScanResult` / `KernelSecurityVulnerability` / `PluginSecurityManifest.scanResults`~~ (`packages/spec/src/kernel/plugin-security-advanced.zod.ts`) | no `.parse`/`.safeParse` site anywhere; **zero** consumers of any kind since the scanner retirement (issue 14919, a number since deleted from the board; live record PR #15930) removed the dead scanner that was the last type-only importer | **CLOSED by removal, #15932** (ruling below). The count this row carried was **22**; the retirement measured **27** authorable rows for the named surface — 8 + 17 for the two defs, plus one each for `PluginSecurityManifest.scanResults` and `PluginQualityMetrics.securityScan`. The 22 is not reconciled, only superseded. |
+| ~~`PluginQualityMetrics.securityScan`~~ (`packages/spec/src/kernel/plugin-registry.zod.ts`) | spec self-test only | **CLOSED by removal, #15932** — tombstoned with the family it mirrored. |
+| Marketplace scan vocab — `marketplace.zod.ts` `'scanning'` status. ⛔ Still live, still unchecked. ⚠️ The two siblings this row used to name are GONE and the coordinates were stale: `marketplace-admin.zod.ts` left with the cloud subpath (#16526), and `incident-response.zod.ts` — which carried `'malware'` on `system/IncidentCategory` — was retired whole by #15513. Both verified as zero tree entries. | declared-only enum member, no producer in this repo | Cloud/EE surface. Same shape as the `'failed'`/`'expired'` upload statuses #7667 had to close: declared, published, no writer. |
| MetadataPlugin FS scan + `metadata-fs` boot scan (`packages/metadata/src/plugin.ts,270` — `watch ?? false`; `packages/runtime/src/standalone-stack.ts` hard-off; `metadata-fs` unwired from any `os dev`/`os serve` lane) | unit-pinned in-package only | No reachable fixture from any shipped boot; if a future lane wires `metadata-fs`, the boot-scan/watcher dot-entry divergence is the risk to test first. |
-The scanner row above was **CLOSED by removal** in #14919 (maintainer ruling,
+**The first two rows above were CLOSED by removal in #15932** (maintainer ruling,
+director seat, decision batch #65, 2026-09-07, adopted verbatim 「同意」): the two defs
+left the build whole (`RETIRED_DEFS_BY_MAJOR[18]`), and the authorable carriers —
+`PluginSecurityManifest.scanResults`, its sibling list `.vulnerabilities` and
+`PluginQualityMetrics.securityScan` — are `retiredKey()` tombstones registered in
+`RETIRED_KEYS_BY_MAJOR[18]`. "Declare an owner to enforce" was refused by name: it would
+rebuild the scanner issue 14919 had just retired for the same reason. ⛔ Do not re-derive it.
+
+⚠️ **The third row is NOT closed and was NOT checked — and it is now ONE member, not
+three.** The ruling made the marketplace `'scanning'` status and the incident
+`'malware'` type conditional on a producer grep of `objectstack-ai/cloud`, and that
+repository was not reachable from the session that executed #15932 — so `'scanning'` is
+untouched, and its absence from a diff is not evidence about it. The ready-to-run command
+is on #15932.
+
+⚠️ **But `'malware'` was already gone when that ruling was written**, which the ruling
+could not have known. It 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 (see
+`incident-response-family-retired`). `marketplace-admin.zod.ts` left with the cloud
+subpath (#16526). Verified on this tree by shape, not taken on report: both files return
+zero tree entries on this branch and on `origin/main`, and no `*.zod.ts` names
+`malware` at all — against a lit control where `'scanning'` returns a live declaration
+in `marketplace.zod.ts`. ⇒ The cloud grep this row still owes is about **one** enum
+member, and ⛔ that one is still genuinely unmeasured.
+
+The scanner row further down was **CLOSED by removal** in PR #15930, for issue 14919 (maintainer ruling,
director summon #14, decision batch #42): the class, its barrel export, its
`packages/core/examples/` demonstration and the `PHASE2_IMPLEMENTATION.md` section that
advertised it are gone, and that section now states plainly that plugin security scanning
diff --git a/packages/spec/api-surface/kernel.json b/packages/spec/api-surface/kernel.json
index d298d04266f..951abcd5f5a 100644
--- a/packages/spec/api-surface/kernel.json
+++ b/packages/spec/api-surface/kernel.json
@@ -142,12 +142,6 @@
"KernelSecurityPolicy (type)",
"KernelSecurityPolicyParsed (type)",
"KernelSecurityPolicySchema (const)",
- "KernelSecurityScanResult (type)",
- "KernelSecurityScanResultParsed (type)",
- "KernelSecurityScanResultSchema (const)",
- "KernelSecurityVulnerability (type)",
- "KernelSecurityVulnerabilityParsed (type)",
- "KernelSecurityVulnerabilitySchema (const)",
"LintableAuthoringCollection (interface)",
"ListPackagesRequest (type)",
"ListPackagesRequestSchema (const)",
diff --git a/packages/spec/authorable-defaults/kernel.json b/packages/spec/authorable-defaults/kernel.json
index 1f7d76063f4..e4997e515e6 100644
--- a/packages/spec/authorable-defaults/kernel.json
+++ b/packages/spec/authorable-defaults/kernel.json
@@ -51,8 +51,6 @@
"kernel/InstalledPackage:status = \"installed\"",
"kernel/KernelContext:features = {}",
"kernel/KernelContext:mode = \"production\"",
- "kernel/KernelSecurityVulnerability:exploitAvailable = false",
- "kernel/KernelSecurityVulnerability:patchAvailable = false",
"kernel/Manifest:defaultDatasource = \"default\"",
"kernel/Manifest:scope = \"project\"",
"kernel/MetadataDiffItem:hasConflict = false",
diff --git a/packages/spec/authorable-surface/kernel.json b/packages/spec/authorable-surface/kernel.json
index 7b06132c37e..b2e931f40eb 100644
--- a/packages/spec/authorable-surface/kernel.json
+++ b/packages/spec/authorable-surface/kernel.json
@@ -251,31 +251,6 @@
"kernel/KernelSecurityPolicy:csp",
"kernel/KernelSecurityPolicy:encryption",
"kernel/KernelSecurityPolicy:rateLimit",
- "kernel/KernelSecurityScanResult:codeIssues",
- "kernel/KernelSecurityScanResult:dependencyVulnerabilities",
- "kernel/KernelSecurityScanResult:licenseCompliance",
- "kernel/KernelSecurityScanResult:scanner",
- "kernel/KernelSecurityScanResult:status",
- "kernel/KernelSecurityScanResult:summary",
- "kernel/KernelSecurityScanResult:timestamp",
- "kernel/KernelSecurityScanResult:vulnerabilities",
- "kernel/KernelSecurityVulnerability:affectedVersions",
- "kernel/KernelSecurityVulnerability:category",
- "kernel/KernelSecurityVulnerability:cve",
- "kernel/KernelSecurityVulnerability:cvssScore",
- "kernel/KernelSecurityVulnerability:description",
- "kernel/KernelSecurityVulnerability:discoveredDate",
- "kernel/KernelSecurityVulnerability:exploitAvailable",
- "kernel/KernelSecurityVulnerability:fixedIn",
- "kernel/KernelSecurityVulnerability:id",
- "kernel/KernelSecurityVulnerability:location",
- "kernel/KernelSecurityVulnerability:patchAvailable",
- "kernel/KernelSecurityVulnerability:publishedDate",
- "kernel/KernelSecurityVulnerability:references",
- "kernel/KernelSecurityVulnerability:remediation",
- "kernel/KernelSecurityVulnerability:severity",
- "kernel/KernelSecurityVulnerability:title",
- "kernel/KernelSecurityVulnerability:workaround",
"kernel/ListPackagesRequest:enabled",
"kernel/ListPackagesRequest:status",
"kernel/ListPackagesRequest:type",
@@ -533,7 +508,7 @@
"kernel/PluginQualityMetrics:codeQuality",
"kernel/PluginQualityMetrics:conformanceTests",
"kernel/PluginQualityMetrics:documentationScore",
- "kernel/PluginQualityMetrics:securityScan",
+ "kernel/PluginQualityMetrics:securityScan [RETIRED]",
"kernel/PluginQualityMetrics:testCoverage",
"kernel/PluginRegistryEntry:capabilities",
"kernel/PluginRegistryEntry:category",
@@ -574,10 +549,10 @@
"kernel/PluginSecurityManifest:pluginId",
"kernel/PluginSecurityManifest:policy",
"kernel/PluginSecurityManifest:sandbox",
- "kernel/PluginSecurityManifest:scanResults",
+ "kernel/PluginSecurityManifest:scanResults [RETIRED]",
"kernel/PluginSecurityManifest:securityContact",
"kernel/PluginSecurityManifest:trustLevel",
- "kernel/PluginSecurityManifest:vulnerabilities",
+ "kernel/PluginSecurityManifest:vulnerabilities [RETIRED]",
"kernel/PluginSecurityManifest:vulnerabilityDisclosure",
"kernel/PluginStartupResult:duration [RETIRED]",
"kernel/PluginStartupResult:durationMs",
diff --git a/packages/spec/declaration-map/kernel.json b/packages/spec/declaration-map/kernel.json
index 10ff42c6b22..92f5db85d9f 100644
--- a/packages/spec/declaration-map/kernel.json
+++ b/packages/spec/declaration-map/kernel.json
@@ -95,10 +95,6 @@
"KernelContextSchema": "kernel/KernelContext",
"KernelSecurityPolicy": "kernel/KernelSecurityPolicy",
"KernelSecurityPolicySchema": "kernel/KernelSecurityPolicy",
- "KernelSecurityScanResult": "kernel/KernelSecurityScanResult",
- "KernelSecurityScanResultSchema": "kernel/KernelSecurityScanResult",
- "KernelSecurityVulnerability": "kernel/KernelSecurityVulnerability",
- "KernelSecurityVulnerabilitySchema": "kernel/KernelSecurityVulnerability",
"ListPackagesRequest": "kernel/ListPackagesRequest",
"ListPackagesRequestSchema": "kernel/ListPackagesRequest",
"ListPackagesResponse": "kernel/ListPackagesResponse",
diff --git a/packages/spec/export-origins/kernel.json b/packages/spec/export-origins/kernel.json
index c66b7b3da0f..7ba8764d433 100644
--- a/packages/spec/export-origins/kernel.json
+++ b/packages/spec/export-origins/kernel.json
@@ -141,12 +141,6 @@
"KernelSecurityPolicy": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityPolicy (type)",
"KernelSecurityPolicyParsed": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityPolicyParsed (type)",
"KernelSecurityPolicySchema": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityPolicySchema (const)",
- "KernelSecurityScanResult": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityScanResult (type)",
- "KernelSecurityScanResultParsed": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityScanResultParsed (type)",
- "KernelSecurityScanResultSchema": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityScanResultSchema (const)",
- "KernelSecurityVulnerability": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityVulnerability (type)",
- "KernelSecurityVulnerabilityParsed": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityVulnerabilityParsed (type)",
- "KernelSecurityVulnerabilitySchema": "src/kernel/plugin-security-advanced.zod.ts#KernelSecurityVulnerabilitySchema (const)",
"LintableAuthoringCollection": "src/kernel/metadata-authoring-lint.ts#LintableAuthoringCollection (interface)",
"ListPackagesRequest": "src/kernel/package-registry.zod.ts#ListPackagesRequest (type)",
"ListPackagesRequestSchema": "src/kernel/package-registry.zod.ts#ListPackagesRequestSchema (const)",
diff --git a/packages/spec/json-schema.manifest/kernel.json b/packages/spec/json-schema.manifest/kernel.json
index 4fc83d18c37..93d80ccb801 100644
--- a/packages/spec/json-schema.manifest/kernel.json
+++ b/packages/spec/json-schema.manifest/kernel.json
@@ -50,8 +50,6 @@
"kernel/InstalledPackage",
"kernel/KernelContext",
"kernel/KernelSecurityPolicy",
- "kernel/KernelSecurityScanResult",
- "kernel/KernelSecurityVulnerability",
"kernel/ListPackagesRequest",
"kernel/ListPackagesResponse",
"kernel/Manifest",
diff --git a/packages/spec/src/kernel/plugin-registry.test.ts b/packages/spec/src/kernel/plugin-registry.test.ts
index 9b419f10e5b..34a433b6556 100644
--- a/packages/spec/src/kernel/plugin-registry.test.ts
+++ b/packages/spec/src/kernel/plugin-registry.test.ts
@@ -111,43 +111,12 @@ describe('Plugin Registry Schemas', () => {
expect(() => PluginQualityMetricsSchema.parse({ codeQuality: -5 })).toThrow();
});
- it('should accept valid security scan', () => {
- const metrics = PluginQualityMetricsSchema.parse({
- securityScan: {
- lastScanDate: '2024-01-15T10:00:00Z',
- vulnerabilities: {
- critical: 0,
- high: 1,
- medium: 2,
- low: 5,
- },
- passed: true,
- },
- });
- expect(metrics.securityScan?.passed).toBe(true);
- expect(metrics.securityScan?.vulnerabilities?.high).toBe(1);
- });
-
- it('should apply defaults in security scan vulnerabilities', () => {
- const metrics = PluginQualityMetricsSchema.parse({
- securityScan: {
- vulnerabilities: {},
- },
- });
- expect(metrics.securityScan?.vulnerabilities?.critical).toBe(0);
- expect(metrics.securityScan?.vulnerabilities?.high).toBe(0);
- expect(metrics.securityScan?.vulnerabilities?.medium).toBe(0);
- expect(metrics.securityScan?.vulnerabilities?.low).toBe(0);
- expect(metrics.securityScan?.passed).toBe(false);
- });
-
- it('should reject negative vulnerability counts', () => {
- expect(() => PluginQualityMetricsSchema.parse({
- securityScan: {
- vulnerabilities: { critical: -1, high: 0, medium: 0, low: 0 },
- },
- })).toThrow();
- });
+ // The three `securityScan` cases that stood here left with the key
+ // (#15932, ADR-0049 enforce-or-remove). Their replacement is not a
+ // narrower version of them: it is the refusal, pinned in
+ // `plugin-security-scan-result-retirement.test.ts` beside the rest of the
+ // family. ⛔ Do not re-add a "valid security scan" case — authoring the key
+ // is now a tsc error (input type `never`) and a parse error.
it('should accept valid conformance tests', () => {
const metrics = PluginQualityMetricsSchema.parse({
diff --git a/packages/spec/src/kernel/plugin-registry.zod.ts b/packages/spec/src/kernel/plugin-registry.zod.ts
index 8517489c6b8..a2cfc0ba4d7 100644
--- a/packages/spec/src/kernel/plugin-registry.zod.ts
+++ b/packages/spec/src/kernel/plugin-registry.zod.ts
@@ -16,6 +16,19 @@ import { MAJOR_MINOR_PATCH_VERSION_PATTERN } from './version-grammar';
* Plugin Vendor Information
*/
import { lazySchema } from '../shared/lazy-schema';
+import { retiredKey } from '../shared/retired-key';
+
+const SECURITY_SCAN_RETIRED =
+ '`PluginQualityMetrics.securityScan` was removed in @objectstack/spec 17 (ADR-0049 '
+ + 'enforce-or-remove) — the block declared a last-scan date, per-severity vulnerability '
+ + 'counts and a `passed` verdict, and no scanner, registry, installer or UI ever produced '
+ + 'or read one, so a plugin could publish `passed: true` with nothing at all behind it. '
+ + 'Delete the key. There is no replacement key: plugin security scanning is not a platform '
+ + 'capability, and the scan-result family this block mirrored was retired with it. The '
+ + 'sibling quality metrics — `testCoverage`, `documentationScore`, `codeQuality` and '
+ + '`conformanceTests` — are unchanged. Audit dependencies with a tool built for it (npm '
+ + 'audit, pnpm audit, Dependabot, the GitHub Advisory Database, OSV).';
+
export const PluginVendorSchema = lazySchema(() => z.object({
/**
* Vendor identifier (reverse domain notation)
@@ -71,18 +84,15 @@ export const PluginQualityMetricsSchema = lazySchema(() => z.object({
codeQuality: z.number().min(0).max(100).optional(),
/**
- * Security scan status
+ * Tombstone: the plugin security-scan status is RETIRED (#15932, ADR-0049
+ * enforce-or-remove), with the `plugin-security-advanced` scan-result family
+ * it mirrored. Not a bare deletion: `PluginQualityMetricsSchema` is not
+ * `.strict()`, so zod would strip an authored key in silence (ADR-0104).
+ * The key itself carried no default, so there is no materialized residue in
+ * previously built artifacts to accept (the inner defaults only ever fired
+ * for an author who wrote the block).
*/
- securityScan: z.object({
- lastScanDate: z.string().datetime().optional(),
- vulnerabilities: z.object({
- critical: z.number().int().min(0).default(0),
- high: z.number().int().min(0).default(0),
- medium: z.number().int().min(0).default(0),
- low: z.number().int().min(0).default(0),
- }).optional(),
- passed: z.boolean().default(false),
- }).optional(),
+ securityScan: retiredKey(SECURITY_SCAN_RETIRED),
/**
* Conformance test results
diff --git a/packages/spec/src/kernel/plugin-security-advanced.zod.ts b/packages/spec/src/kernel/plugin-security-advanced.zod.ts
index 37b60343a7a..bf87e3c32fd 100644
--- a/packages/spec/src/kernel/plugin-security-advanced.zod.ts
+++ b/packages/spec/src/kernel/plugin-security-advanced.zod.ts
@@ -13,8 +13,15 @@ import { EvaluatedExpressionInputSchema } from '../shared/expression.zod';
* - Fine-grained permission system
* - Resource access control
* - Sandboxing and isolation
- * - Security scanning and verification
* - Runtime security monitoring
+ *
+ * ⛔ NOT security scanning. The scan-result family — `KernelSecurityScanResult`,
+ * `KernelSecurityVulnerability`, `PluginSecurityManifest.scanResults` and its
+ * sibling list `.vulnerabilities` — was retired under ADR-0049 enforce-or-remove
+ * (#15932), after the runtime scanner that was its last type-only importer went
+ * the same way. That scanner retirement was issue 14919 — a number DELETED from
+ * the board and no longer resolving; its live record is PR #15930, which landed
+ * the removal. Nothing on this platform scans a plugin.
*/
/**
@@ -346,6 +353,30 @@ const DISCLOSURE_RESPONSE_TIME_RETIRED =
+ 'name on `PluginHealthReport.metrics` was milliseconds — which is the confusion the rule '
+ 'exists to remove. Rename the key to `responseTimeHours`; the value (hours) is unchanged.';
+// The scan-result family's two tombstones (#15932, ADR-0049 enforce-or-remove).
+// Their value types — `KernelSecurityScanResult` and `KernelSecurityVulnerability` —
+// are gone from this build entirely; see the header note above.
+const SCAN_RESULTS_RETIRED =
+ '`PluginSecurityManifest.scanResults` was removed in @objectstack/spec 17 (ADR-0049 '
+ + 'enforce-or-remove) — nothing on this platform ever produced, stored or read a security '
+ + 'scan result, so an authored array parsed cleanly and changed nothing. Delete the key. '
+ + 'There is no replacement key: plugin security scanning is not a platform capability. What '
+ + 'this manifest still enforces is `permissions` and `sandbox`; artifact provenance is '
+ + 'answered by the plugin signature verifier, which tells you an artifact is the one its '
+ + 'publisher signed and never that it is safe. Audit dependencies with a tool built for it '
+ + '(npm audit, pnpm audit, Dependabot, the GitHub Advisory Database, OSV).';
+
+const MANIFEST_VULNERABILITIES_RETIRED =
+ '`PluginSecurityManifest.vulnerabilities` was removed in @objectstack/spec 17 (ADR-0049 '
+ + 'enforce-or-remove) — it was an array of `KernelSecurityVulnerability`, the other half of '
+ + 'the scan-result family, and leaves with it: nothing ever wrote the list and nothing ever '
+ + 'read it, so declaring a known vulnerability against a plugin warned nobody and blocked '
+ + 'no install. Delete the key. There is no replacement key: plugin security scanning is not '
+ + 'a platform capability. Publish vulnerability contact and disclosure terms through the '
+ + 'surviving `securityContact` and `vulnerabilityDisclosure` blocks on this same manifest, '
+ + 'and audit dependencies with a tool built for it (npm audit, pnpm audit, Dependabot, the '
+ + 'GitHub Advisory Database, OSV).';
+
/**
* Sandbox Configuration
* Defines how plugin is isolated
@@ -434,171 +465,6 @@ export const SandboxConfigSchema = lazySchema(() => z.object({
}).optional(),
}));
-/**
- * Security Vulnerability
- * Represents a known security vulnerability
- */
-export const KernelSecurityVulnerabilitySchema = lazySchema(() => z.object({
- /**
- * CVE identifier
- */
- cve: z.string().optional(),
-
- /**
- * Vulnerability identifier
- */
- id: z.string(),
-
- /**
- * Severity level
- */
- severity: z.enum(['critical', 'high', 'medium', 'low', 'info']),
-
- /**
- * Category (e.g., SAST, DAST, Dependency)
- */
- category: z.string().optional(),
-
- /**
- * Title
- */
- title: z.string(),
-
- /**
- * Location of the vulnerability
- */
- location: z.string().optional(),
-
- /**
- * Remediation steps
- */
- remediation: z.string().optional(),
-
- /**
- * Description
- */
- description: z.string(),
-
- /**
- * Affected versions
- */
- affectedVersions: z.array(z.string()),
-
- /**
- * Fixed in versions
- */
- fixedIn: z.array(z.string()).optional(),
-
- /**
- * CVSS score
- */
- cvssScore: z.number().min(0).max(10).optional(),
-
- /**
- * Exploit availability
- */
- exploitAvailable: z.boolean().default(false),
-
- /**
- * Patch available
- */
- patchAvailable: z.boolean().default(false),
-
- /**
- * Workaround
- */
- workaround: z.string().optional(),
-
- /**
- * References
- */
- references: z.array(z.string()).optional(),
-
- /**
- * Discovered date
- */
- discoveredDate: z.string().datetime().optional(),
-
- /**
- * Published date
- */
- publishedDate: z.string().datetime().optional(),
-}));
-
-/**
- * Security Scan Result
- * Result of security scanning
- */
-export const KernelSecurityScanResultSchema = lazySchema(() => z.object({
- /**
- * Scan timestamp
- */
- timestamp: z.string().datetime(),
-
- /**
- * Scanner information
- */
- scanner: z.object({
- name: z.string(),
- version: z.string(),
- }),
-
- /**
- * Overall status
- */
- status: z.enum(['passed', 'failed', 'warning']),
-
- /**
- * Vulnerabilities found
- */
- vulnerabilities: z.array(KernelSecurityVulnerabilitySchema).optional(),
-
- /**
- * Code quality issues
- */
- codeIssues: z.array(z.object({
- severity: z.enum(['error', 'warning', 'info']),
- type: z.string().describe('Issue type (e.g., sql-injection, xss)'),
- file: z.string(),
- line: z.number().int().optional(),
- message: z.string(),
- suggestion: z.string().optional(),
- })).optional(),
-
- /**
- * Dependency vulnerabilities
- */
- dependencyVulnerabilities: z.array(z.object({
- package: z.string(),
- version: z.string(),
- vulnerability: KernelSecurityVulnerabilitySchema,
- })).optional(),
-
- /**
- * License compliance
- */
- licenseCompliance: z.object({
- status: z.enum(['compliant', 'non-compliant', 'unknown']),
- issues: z.array(z.object({
- package: z.string(),
- license: z.string(),
- reason: z.string(),
- })).optional(),
- }).optional(),
-
- /**
- * Summary statistics
- */
- summary: z.object({
- totalVulnerabilities: z.number().int(),
- criticalCount: z.number().int(),
- highCount: z.number().int(),
- mediumCount: z.number().int(),
- lowCount: z.number().int(),
- infoCount: z.number().int(),
- }),
-}));
-
/**
* Security Policy
* Defines security policies for plugin
@@ -721,14 +587,18 @@ export const PluginSecurityManifestSchema = lazySchema(() => z.object({
policy: KernelSecurityPolicySchema.optional(),
/**
- * Security scan results
+ * Tombstone: the scan-result surface is RETIRED (#15932, ADR-0049
+ * enforce-or-remove). `KernelSecurityScanResult` and
+ * `KernelSecurityVulnerability` left this build with it — the last importer
+ * of either type went with `PluginSecurityScanner` (issue 14919, a number
+ * deleted from the board; live record: PR #15930). Not a bare
+ * deletion: this shape is not `.strict()`, so zod would strip an authored
+ * key in silence (ADR-0104).
*/
- scanResults: z.array(KernelSecurityScanResultSchema).optional(),
-
- /**
- * Known vulnerabilities
- */
- vulnerabilities: z.array(KernelSecurityVulnerabilitySchema).optional(),
+ scanResults: retiredKey(SCAN_RESULTS_RETIRED),
+
+ /** Tombstone for the sibling list — same retirement, same reasoning. */
+ vulnerabilities: retiredKey(MANIFEST_VULNERABILITIES_RETIRED),
/**
* Code signing
@@ -793,12 +663,6 @@ export type RuntimeConfigParsed = z.infer;
export type SandboxConfig = z.input;
/** Post-parse shape of {@link SandboxConfig} — defaults applied, transforms run (ADR-0122). */
export type SandboxConfigParsed = z.infer;
-export type KernelSecurityVulnerability = z.input;
-/** Post-parse shape of {@link KernelSecurityVulnerability} — defaults applied, transforms run (ADR-0122). */
-export type KernelSecurityVulnerabilityParsed = z.infer;
-export type KernelSecurityScanResult = z.input;
-/** Post-parse shape of {@link KernelSecurityScanResult} — defaults applied, transforms run (ADR-0122). */
-export type KernelSecurityScanResultParsed = z.infer;
export type KernelSecurityPolicy = z.input;
/** Post-parse shape of {@link KernelSecurityPolicy} — defaults applied, transforms run (ADR-0122). */
export type KernelSecurityPolicyParsed = z.infer;
diff --git a/packages/spec/src/kernel/plugin-security-scan-result-retirement.test.ts b/packages/spec/src/kernel/plugin-security-scan-result-retirement.test.ts
new file mode 100644
index 00000000000..33f307e149e
--- /dev/null
+++ b/packages/spec/src/kernel/plugin-security-scan-result-retirement.test.ts
@@ -0,0 +1,139 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import { describe, it, expect } from 'vitest';
+
+import { PluginSecurityManifestSchema } from './plugin-security-advanced.zod';
+import { PluginQualityMetricsSchema } from './plugin-registry.zod';
+
+// ─── [#15932] the plugin-security SCAN-RESULT family is REMOVED ───────────────
+//
+// ADR-0049 enforce-or-remove; director seat, decision batch #65, 2026-09-07,
+// maintainer verbatim 「同意」. Four members, one family:
+//
+// - `KernelSecurityScanResult` — whole def, out of the build
+// - `KernelSecurityVulnerability` — whole def, out of the build
+// - `PluginSecurityManifest.scanResults` — tombstoned key
+// - `PluginQualityMetrics.securityScan` — tombstoned key
+//
+// plus `PluginSecurityManifest.vulnerabilities`, the last authorable referent of
+// the second def and therefore a forced consequence of retiring it.
+//
+// This is the second half of the scanner retirement (issue 14919, a number since
+// deleted from the board; live record PR #15930), which retired `PluginSecurityScanner` — the
+// `@objectstack/core` class that shipped as a security control and returned
+// `status: "passed"` for every plugin it was ever handed. That scanner was the
+// family's ONLY importer of any kind (a type-only import), so its deletion moved
+// these schemas from one type-only importer to zero consumers while they stayed
+// fully published on the authorable surface.
+//
+// ⚠️ On the assertion set. A schema tombstone raises a `ZodError`, not the
+// ADR-0112 envelope: its issues carry `code` and `path` and have no `status`
+// field at all. So these pins assert the strongest set this surface really has —
+// refusal, the issue `code`, the `path` naming WHICH key was refused, and the
+// prescription text (where the wording is the contract, pin the wording). A bare
+// `toThrow()` would stay green if the refusal moved to the wrong key or stopped
+// carrying the fix.
+
+/** A manifest that is valid except for whatever an individual test adds. */
+const baseManifest = {
+ pluginId: 'com.example.plugin',
+ trustLevel: 'community',
+ permissions: { permissions: [] },
+ sandbox: {},
+} as const;
+
+describe('[#15932] plugin-security scan-result retirement', () => {
+ it('REJECTS an authored `scanResults` array, naming the key and carrying the fix', () => {
+ const result = PluginSecurityManifestSchema.safeParse({
+ ...baseManifest,
+ scanResults: [{ timestamp: '2026-01-15T10:00:00Z', status: 'passed' }],
+ });
+
+ expect(result.success).toBe(false);
+ if (result.success) return; // narrowing; the assertion above already failed
+
+ const issue = result.error.issues.find((i) => i.path[0] === 'scanResults');
+ expect(issue, 'the refusal must name `scanResults`').toBeDefined();
+ expect(issue!.code).toBe('invalid_type');
+ expect(issue!.path).toEqual(['scanResults']);
+ // The prescription IS the migration doc for whoever hits it — contract, not
+ // commentary. It must say the removal happened, and must NOT send the reader
+ // looking for a replacement key that does not exist.
+ expect(issue!.message).toMatch(/`PluginSecurityManifest\.scanResults`.*removed.*ADR-0049/s);
+ expect(issue!.message).toMatch(/Delete the key/s);
+ expect(issue!.message).toMatch(/no replacement key/s);
+ // An author who read a clean scan as evidence of safety has a security
+ // decision to revisit; silence here is what made the inert key dangerous.
+ expect(issue!.message).toMatch(/never that it is safe/s);
+ });
+
+ it('REJECTS the sibling `vulnerabilities` list, which left with its value type', () => {
+ const result = PluginSecurityManifestSchema.safeParse({
+ ...baseManifest,
+ vulnerabilities: [{ id: 'V-1', severity: 'critical', title: 't', description: 'd', affectedVersions: [] }],
+ });
+
+ expect(result.success).toBe(false);
+ if (result.success) return;
+
+ const issue = result.error.issues.find((i) => i.path[0] === 'vulnerabilities');
+ expect(issue, 'the refusal must name `vulnerabilities`').toBeDefined();
+ expect(issue!.code).toBe('invalid_type');
+ expect(issue!.message).toMatch(/`PluginSecurityManifest\.vulnerabilities`.*removed/s);
+ expect(issue!.message).toMatch(/blocked\s+no install/s);
+ });
+
+ it('REJECTS `PluginQualityMetrics.securityScan`, the member that published a VERDICT', () => {
+ const result = PluginQualityMetricsSchema.safeParse({
+ testCoverage: 85,
+ securityScan: { lastScanDate: '2026-01-15T10:00:00Z', passed: true },
+ });
+
+ expect(result.success).toBe(false);
+ if (result.success) return;
+
+ const issue = result.error.issues.find((i) => i.path[0] === 'securityScan');
+ expect(issue, 'the refusal must name `securityScan`').toBeDefined();
+ expect(issue!.code).toBe('invalid_type');
+ expect(issue!.path).toEqual(['securityScan']);
+ expect(issue!.message).toMatch(/`PluginQualityMetrics\.securityScan`.*removed.*ADR-0049/s);
+ // The prescription must say plainly what the verdict was worth, because a
+ // consumer that gated on `passed: true` was gating on nothing.
+ expect(issue!.message).toMatch(/`passed: true` with nothing at all behind it/s);
+ expect(issue!.message).toMatch(/Delete the key/s);
+ });
+
+ it('parses cleanly once the keys are deleted, and grows no such property', () => {
+ // Absence must stay absence. Neither carrying shape is `.strict()`, so a bare
+ // deletion would have stripped an authored key in SILENCE (ADR-0104) — this
+ // pin plus the refusals above are what keep the tombstones honest.
+ const manifest = PluginSecurityManifestSchema.parse({ ...baseManifest });
+ expect(manifest.pluginId).toBe('com.example.plugin');
+ expect(manifest).not.toHaveProperty('scanResults');
+ expect(manifest).not.toHaveProperty('vulnerabilities');
+
+ const metrics = PluginQualityMetricsSchema.parse({ testCoverage: 85, codeQuality: 75 });
+ expect(metrics.testCoverage).toBe(85);
+ expect(metrics).not.toHaveProperty('securityScan');
+ });
+
+ it('does not export the retired scan-result defs from ./kernel', async () => {
+ const kernel = await import('./index');
+ for (const name of ['KernelSecurityScanResultSchema', 'KernelSecurityVulnerabilitySchema']) {
+ expect(kernel, `${name} must not be exported after #15932`).not.toHaveProperty(name);
+ }
+
+ // Anti-vacuity: this pin means nothing unless the barrel really resolved and
+ // still exports the neighbours that SURVIVE. Three groups, each load-bearing:
+ // the manifest's own surviving security vocabulary; the sibling quality
+ // metrics; and — the scope fence — the SEPARATELY DECLARED, unprefixed pair
+ // in `plugin-security.zod.ts`, which is a different family with its own
+ // self-test and is deliberately NOT part of this retirement.
+ expect(kernel).toHaveProperty('PluginSecurityManifestSchema');
+ expect(kernel).toHaveProperty('KernelSecurityPolicySchema');
+ expect(kernel).toHaveProperty('SandboxConfigSchema');
+ expect(kernel).toHaveProperty('PluginQualityMetricsSchema');
+ expect(kernel).toHaveProperty('SecurityScanResultSchema');
+ expect(kernel).toHaveProperty('SecurityVulnerabilitySchema');
+ });
+});
diff --git a/packages/spec/src/migrations/entries/retired-defs/18.kernel__KernelSecurityScanResult.ts b/packages/spec/src/migrations/entries/retired-defs/18.kernel__KernelSecurityScanResult.ts
new file mode 100644
index 00000000000..0c5f1261f89
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-defs/18.kernel__KernelSecurityScanResult.ts
@@ -0,0 +1,30 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65,
+// 2026-09-07, maintainer verbatim 「同意」). `KernelSecurityScanResult` declared a
+// complete scan report — timestamp, scanner name/version, a passed/failed/warning
+// status, vulnerability and code-issue lists, dependency findings, license
+// compliance and a six-number summary — and no layer ever emitted, stored, parsed
+// or read one. Its only importer of any kind was `PluginSecurityScanner`, a
+// type-only import from `packages/core/src/security/security-scanner.ts`, and
+// PR #15930 deleted that file (it closed issue 14919, a number since deleted
+// from the board and no longer resolving); the census after it landed put every remaining
+// reference inside the declaring module itself, against a lit control (five hits
+// for `PluginSecurityManifest` in the same file), so the zero is a reading.
+//
+// Whole-def retirement, not a tombstone: nothing parses this def, so there is no
+// author to hand a prescription to and no `${defKey}:${name}` key leaving a live
+// shape. `RETIRED_DEFS_BY_MAJOR[18]` plus the semantic entry
+// `plugin-security-scan-result-surface-retired` ARE the declaration — the
+// #11825 shape (its sibling in the pair this tree usually cites, issue 8715, is
+// a number deleted from the board; 11825 is the half that still resolves).
+// The two authorable carriers that pointed here,
+// `PluginSecurityManifest.scanResults` and `.vulnerabilities`, are separately
+// tombstoned and registered in `RETIRED_KEYS_BY_MAJOR[18]`.
+//
+// No D2 conversion: a plugin security manifest is a package artifact a publisher
+// ships, never a stack collection member and never a stored `sys_metadata` row,
+// so the conversion chain has no seam that would see one (the sibling
+// `kernel/PluginSecurityManifest:vulnerabilityDisclosure.responseTime` entry
+// records the same reasoning for the same schema).
+export const entry = 'kernel/KernelSecurityScanResult';
diff --git a/packages/spec/src/migrations/entries/retired-defs/18.kernel__KernelSecurityVulnerability.ts b/packages/spec/src/migrations/entries/retired-defs/18.kernel__KernelSecurityVulnerability.ts
new file mode 100644
index 00000000000..142f80b4e63
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-defs/18.kernel__KernelSecurityVulnerability.ts
@@ -0,0 +1,28 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65,
+// 2026-09-07, maintainer verbatim 「同意」). The other half of the scan-result
+// family: a CVE-shaped vulnerability record (severity, CVSS score, affected and
+// fixed versions, exploit/patch availability, remediation, disclosure dates) that
+// nothing ever constructed, validated or consulted. It reached this build through
+// exactly three referents, all now gone — `KernelSecurityScanResult.vulnerabilities`
+// and `.dependencyVulnerabilities[].vulnerability` (that def leaves in the same
+// change) and `PluginSecurityManifest.vulnerabilities` (tombstoned). Retired
+// together with `KernelSecurityScanResult` because declaring a vulnerability
+// vocabulary with no scan to carry it reads as a capability, which is the
+// ADR-0049 shape.
+//
+// Whole-def retirement for the reason its sibling entry records, and the same
+// disposition: `RETIRED_DEFS_BY_MAJOR[18]` plus
+// `plugin-security-scan-result-surface-retired`, no D2 conversion, no tombstone
+// of its own. Its two authorable defaults —
+// `KernelSecurityVulnerability:exploitAvailable = false` and
+// `:patchAvailable = false` — leave `authorable-defaults/kernel.json` with the
+// def; nothing ever parsed this schema, so no released toolchain ever
+// materialized either value into an artifact and there is no residue to accept.
+//
+// ⛔ NOT in scope, and deliberately untouched: the unprefixed
+// `SecurityVulnerabilitySchema` / `SecurityScanResultSchema` pair in the sibling
+// module `kernel/plugin-security.zod.ts`. Those are separate defs with their own
+// self-test and are outside this ruling.
+export const entry = 'kernel/KernelSecurityVulnerability';
diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginQualityMetrics__securityScan.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginQualityMetrics__securityScan.ts
new file mode 100644
index 00000000000..f521009ec56
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginQualityMetrics__securityScan.ts
@@ -0,0 +1,27 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65,
+// 2026-09-07, maintainer verbatim 「同意」). `PluginQualityMetrics.securityScan` is
+// the scan-result family's sibling on the plugin registry entry, named by the
+// ruling alongside it: a last-scan date, per-severity vulnerability counts and a
+// `passed` boolean, read by no scanner, registry, installer or UI. The census put
+// every reference in `packages/spec/src/kernel/plugin-registry.test.ts` — the
+// spec's own self-test and nothing else.
+//
+// It is the sharper half of the family for an author: `scanResults` published a
+// report, but `securityScan.passed` published a VERDICT, so a plugin could ship
+// `passed: true` with nothing at all behind it and a consumer reading the
+// registry entry had no way to tell that from a real result.
+//
+// Tombstoned with `retiredKey()`: `PluginQualityMetricsSchema` is a plain
+// `z.object`, so a bare deletion would strip an authored block in silence
+// (ADR-0104). The key carried NO default of its own — the defaults inside it
+// (`critical`/`high`/`medium`/`low = 0`, `passed = false`) fired only for an
+// author who wrote the block — so `acceptRetiredDefaultResidue` is not owed:
+// there is no value a released toolchain materialized into an artifact whose
+// author never typed the key.
+//
+// No D2 conversion: a plugin registry entry is a published package artifact, not
+// a stack collection member or a stored `sys_metadata` row. The ledger channel is
+// `plugin-security-scan-result-surface-retired`.
+export const entry = 'kernel/PluginQualityMetrics:securityScan';
diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginSecurityManifest__scanResults.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginSecurityManifest__scanResults.ts
new file mode 100644
index 00000000000..fee45f4b620
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginSecurityManifest__scanResults.ts
@@ -0,0 +1,25 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65,
+// 2026-09-07, maintainer verbatim 「同意」). `PluginSecurityManifest.scanResults`
+// published an array of `KernelSecurityScanResult` on the authorable surface with
+// zero authors and zero parsers: no `.parse`/`.safeParse` site existed anywhere
+// against the scan-result schemas, so a publisher could declare a clean scan on a
+// plugin manifest, be accepted, and get nothing — the declared-not-enforced shape
+// Prime Directive #10 names, one layer out from the runtime scanner that PR
+// #15930 removed for the same reason (that PR closed issue 14919, a number since
+// deleted from the board and no longer resolving).
+//
+// Tombstoned with `retiredKey()`, not deleted: `PluginSecurityManifestSchema` is a
+// plain `z.object`, not `.strict()`, so a bare deletion would strip an authored
+// key in silence (ADR-0104) — swapping an inert declaration for an invisible one.
+// The value type leaves this build entirely (`RETIRED_DEFS_BY_MAJOR[18]`,
+// `kernel/KernelSecurityScanResult`).
+//
+// No D2 conversion: a security manifest is a package artifact a publisher ships,
+// never a stack collection member and never a stored `sys_metadata` row, so the
+// chain has no seam that would see one — the disposition the sibling
+// `vulnerabilityDisclosure.responseTime` entry on this same schema already
+// records. The prescription an author meets is the tombstone itself; the ledger
+// channel is `plugin-security-scan-result-surface-retired`.
+export const entry = 'kernel/PluginSecurityManifest:scanResults';
diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginSecurityManifest__vulnerabilities.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginSecurityManifest__vulnerabilities.ts
new file mode 100644
index 00000000000..e71c5ea25e2
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginSecurityManifest__vulnerabilities.ts
@@ -0,0 +1,22 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #15932 — ADR-0049 enforce-or-remove, decision batch #65.
+// `PluginSecurityManifest.vulnerabilities` was an array of
+// `KernelSecurityVulnerability` and is this retirement's FORCED CONSEQUENCE
+// rather than a name the ruling listed: it was the last authorable referent of a
+// def the ruling retires by name, so it cannot survive the def, and keeping the
+// def alive only to carry it would be keeping the retired family alive under a
+// second name. It is inert on its own terms too — nothing ever wrote the list and
+// nothing ever read it, so declaring a known vulnerability against a plugin
+// warned nobody and blocked no install.
+//
+// ⚠️ This is the ONE key outside the four names the #15932 dispatch fenced
+// (`KernelSecurityScanResult`, `KernelSecurityVulnerability`,
+// `PluginSecurityManifest.scanResults`, `PluginQualityMetrics.securityScan`), and
+// it is reported as such on the card and in the landing PR rather than absorbed
+// silently. It is not a neighbour retired by proximity — the fence's stated
+// concern — it is a referent of a named retiree.
+//
+// Tombstoned with `retiredKey()` for the reason its `scanResults` sibling records
+// (non-strict shape, ADR-0104 silent strip). No D2 conversion, same reasoning.
+export const entry = 'kernel/PluginSecurityManifest:vulnerabilities';
diff --git a/packages/spec/src/migrations/entries/semantic/18.plugin-security-scan-result-surface-retired.ts b/packages/spec/src/migrations/entries/semantic/18.plugin-security-scan-result-surface-retired.ts
new file mode 100644
index 00000000000..c531c10f9f8
--- /dev/null
+++ b/packages/spec/src/migrations/entries/semantic/18.plugin-security-scan-result-surface-retired.ts
@@ -0,0 +1,95 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import type { SemanticMigration } from '../../types.js';
+
+export const entry: SemanticMigration = {
+ id: 'plugin-security-scan-result-surface-retired',
+ // No backticks in `surface` — build-upgrade-guide.ts renders it inside a code
+ // span AND a table cell.
+ surface: 'the plugin-security scan-result family: the defs '
+ + 'KernelSecurityScanResult and KernelSecurityVulnerability '
+ + '(kernel/plugin-security-advanced.zod.ts), their two authorable carriers on '
+ + 'PluginSecurityManifest — scanResults and vulnerabilities — and the sibling '
+ + 'verdict block PluginQualityMetrics.securityScan (kernel/plugin-registry.zod.ts)',
+ replacement:
+ 'nothing to re-declare — delete the keys and every import of the two types. Plugin '
+ + 'security scanning is not a platform capability and there is no replacement schema. '
+ + 'What the platform does still enforce, and what to reach for instead: `permissions` and '
+ + '`sandbox` on the same PluginSecurityManifest are unchanged, and artifact provenance is '
+ + 'answered by `verifyPluginArtifactIntegrity` and the plugin signature verifier — which '
+ + 'tell you an artifact is the one its publisher signed, and never that it is safe. For '
+ + 'dependency vulnerabilities use the tools built for it against your own project (npm '
+ + 'audit / pnpm audit, Dependabot, the GitHub Advisory Database, OSV) and treat an '
+ + 'unaudited third-party plugin as untrusted code. A publisher who used scanResults to '
+ + 'advertise diligence keeps the surviving securityContact and vulnerabilityDisclosure '
+ + 'blocks, which are contact terms rather than a verdict.',
+ reason:
+ 'ADR-0049 enforce-or-remove; maintainer ruling 2026-09-07 on #15932 (director seat, decision batch #65, adopted verbatim 「同意」). '
+ + 'This is the second half of the scanner retirement — issue 14919, a number since '
+ + 'DELETED from the board, landed as PR #15930. That card retired PluginSecurityScanner — a '
+ + '@objectstack/core class that shipped as a SECURITY control and could not fail, whose '
+ + 'verdict was status "passed" for every plugin it was ever handed. The SCHEMAS the '
+ + 'scanner fed survived it, and the scanner had been their only importer of any kind (a '
+ + 'type-only import in packages/core/src/security/security-scanner.ts), so the family went '
+ + 'from one type-only importer to zero consumers while staying fully published: 27 '
+ + 'authorable rows across kernel.json, six api-surface exports, two authorable defaults '
+ + 'and two json-schema manifest keys. An author could write any of it, be accepted, and '
+ + 'get nothing — declared-not-enforced, Prime Directive #10, one layer out from the class '
+ + 'removed for the same reason. The census was taken on origin/main after that removal landed, '
+ + 'with a lit control (five hits for PluginSecurityManifest inside the declaring module) '
+ + 'proving the file greppable, and found no .parse or .safeParse site against either '
+ + 'schema anywhere in packages/**. securityScan is the sharpest member: scanResults '
+ + 'published a report, but securityScan.passed published a VERDICT, so a plugin could '
+ + 'declare itself clean with nothing behind it. Route: the two defs leave the build whole '
+ + '(RETIRED_DEFS_BY_MAJOR[18]) because nothing parses them and a prescription nobody can '
+ + 'receive is not worth its cost; the three authorable keys are retiredKey() tombstones '
+ + '(RETIRED_KEYS_BY_MAJOR[18]) because both carrying shapes are non-strict, where a bare '
+ + 'deletion is a silent strip (ADR-0104). Why this entry and not a 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, so the conversion '
+ + 'chain has no seam that would see one — the disposition the sibling '
+ + 'kernel-plugin-security-durations-unit-in-key entry already records for this same '
+ + 'manifest. No deprecation window (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」). '
+ + 'Scope note, recorded rather than acted on: PluginSecurityManifest.vulnerabilities is a '
+ + 'forced consequence rather than a name the ruling listed — it was the last authorable '
+ + 'referent of KernelSecurityVulnerability and could not outlive the def. Two neighbours '
+ + 'the ruling made CONDITIONAL are deliberately untouched here because the repository the '
+ + 'condition names, objectstack-ai/cloud, is not reachable from the session that executed '
+ + 'this: the marketplace "scanning" status stays exactly as it is — unremoved, and NOT '
+ + 'recorded as checked. Its two siblings were MEASURED rather than assumed, and the '
+ + 'record is corrected here: both were ALREADY GONE when that ruling was written. 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); see '
+ + 'incident-response-family-retired. And marketplace-admin.zod.ts was deleted outright '
+ + 'with the cloud subpath (#16526); see cloud-subpath-retired. Verified on this tree by '
+ + 'shape: both files return zero tree entries and no *.zod.ts names malware at all, '
+ + 'against a lit control where "scanning" still returns a live declaration in '
+ + 'marketplace.zod.ts. So the conditional question is ONE enum member wide, not three, '
+ + 'and the objectstack-ai/cloud producer grep it still owes is that much smaller. '
+ + '⚠️ The out-of-repo consumer population is NOT MEASURED. @objectstack/spec is published, '
+ + 'so this removal is breaking for consumers no download, dependent or source telemetry '
+ + 'was consulted for — accepted as an input to the ruling, exactly as that retirement states '
+ + 'of its own three exports, and not a reason to soften the removal. #15932, PR #15930 '
+ + '(for the deleted issue 14919), ADR-0049, ADR-0087.',
+ acceptanceCriteria:
+ 'No source imports KernelSecurityScanResult, KernelSecurityVulnerability or either '
+ + 'Schema from @objectstack/spec/kernel: both defs are absent from the built kernel '
+ + 'barrel and from api-surface/kernel.json, so a TypeScript consumer gets the refusal at '
+ + 'compile time at the import site rather than a missing runtime value. Authoring '
+ + 'PluginSecurityManifest.scanResults, PluginSecurityManifest.vulnerabilities or '
+ + 'PluginQualityMetrics.securityScan fails to compile (input type `never`) and fails to '
+ + 'parse with the tombstone prescription naming that key — verified by refusal pins that '
+ + 'assert the issue code, the path naming WHICH key was refused, and the prescription '
+ + 'text, plus a positive pin that the surrounding manifest still parses and grows no such '
+ + 'property. ⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: '
+ + 'nothing ever read any of these keys, so deleting one removes no check that was running. '
+ + 'A publisher who believed a declared scanResults entry gated anything was never getting '
+ + 'that gate; the remediation is to audit with a real tool, not to find a replacement key. '
+ + 'The surviving neighbours must still parse and still be exported — permissions, sandbox, '
+ + 'policy, codeSigning, certifications, securityContact and vulnerabilityDisclosure on the '
+ + 'manifest, testCoverage/documentationScore/codeQuality/conformanceTests on the quality '
+ + 'metrics, and the separately-declared SecurityScanResultSchema / '
+ + 'SecurityVulnerabilitySchema in kernel/plugin-security.zod.ts, which this change does '
+ + 'not touch.',
+};
diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts
index f8ecd4f84a4..fa8f1e18e9d 100644
--- a/packages/spec/src/migrations/registry.ts
+++ b/packages/spec/src/migrations/registry.ts
@@ -10618,6 +10618,97 @@ const step18: MigrationStep = {
+ 'still carries `globs` keeps serving as stored data; clear it by deleting the key from '
+ 'the source manifest and republishing.',
},
+ {
+ id: 'plugin-security-scan-result-surface-retired',
+ // No backticks in `surface` — build-upgrade-guide.ts renders it inside a code
+ // span AND a table cell.
+ surface: 'the plugin-security scan-result family: the defs '
+ + 'KernelSecurityScanResult and KernelSecurityVulnerability '
+ + '(kernel/plugin-security-advanced.zod.ts), their two authorable carriers on '
+ + 'PluginSecurityManifest — scanResults and vulnerabilities — and the sibling '
+ + 'verdict block PluginQualityMetrics.securityScan (kernel/plugin-registry.zod.ts)',
+ replacement:
+ 'nothing to re-declare — delete the keys and every import of the two types. Plugin '
+ + 'security scanning is not a platform capability and there is no replacement schema. '
+ + 'What the platform does still enforce, and what to reach for instead: `permissions` and '
+ + '`sandbox` on the same PluginSecurityManifest are unchanged, and artifact provenance is '
+ + 'answered by `verifyPluginArtifactIntegrity` and the plugin signature verifier — which '
+ + 'tell you an artifact is the one its publisher signed, and never that it is safe. For '
+ + 'dependency vulnerabilities use the tools built for it against your own project (npm '
+ + 'audit / pnpm audit, Dependabot, the GitHub Advisory Database, OSV) and treat an '
+ + 'unaudited third-party plugin as untrusted code. A publisher who used scanResults to '
+ + 'advertise diligence keeps the surviving securityContact and vulnerabilityDisclosure '
+ + 'blocks, which are contact terms rather than a verdict.',
+ reason:
+ 'ADR-0049 enforce-or-remove; maintainer ruling 2026-09-07 on #15932 (director seat, decision batch #65, adopted verbatim 「同意」). '
+ + 'This is the second half of the scanner retirement — issue 14919, a number since '
+ + 'DELETED from the board, landed as PR #15930. That card retired PluginSecurityScanner — a '
+ + '@objectstack/core class that shipped as a SECURITY control and could not fail, whose '
+ + 'verdict was status "passed" for every plugin it was ever handed. The SCHEMAS the '
+ + 'scanner fed survived it, and the scanner had been their only importer of any kind (a '
+ + 'type-only import in packages/core/src/security/security-scanner.ts), so the family went '
+ + 'from one type-only importer to zero consumers while staying fully published: 27 '
+ + 'authorable rows across kernel.json, six api-surface exports, two authorable defaults '
+ + 'and two json-schema manifest keys. An author could write any of it, be accepted, and '
+ + 'get nothing — declared-not-enforced, Prime Directive #10, one layer out from the class '
+ + 'removed for the same reason. The census was taken on origin/main after that removal landed, '
+ + 'with a lit control (five hits for PluginSecurityManifest inside the declaring module) '
+ + 'proving the file greppable, and found no .parse or .safeParse site against either '
+ + 'schema anywhere in packages/**. securityScan is the sharpest member: scanResults '
+ + 'published a report, but securityScan.passed published a VERDICT, so a plugin could '
+ + 'declare itself clean with nothing behind it. Route: the two defs leave the build whole '
+ + '(RETIRED_DEFS_BY_MAJOR[18]) because nothing parses them and a prescription nobody can '
+ + 'receive is not worth its cost; the three authorable keys are retiredKey() tombstones '
+ + '(RETIRED_KEYS_BY_MAJOR[18]) because both carrying shapes are non-strict, where a bare '
+ + 'deletion is a silent strip (ADR-0104). Why this entry and not a 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, so the conversion '
+ + 'chain has no seam that would see one — the disposition the sibling '
+ + 'kernel-plugin-security-durations-unit-in-key entry already records for this same '
+ + 'manifest. No deprecation window (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」). '
+ + 'Scope note, recorded rather than acted on: PluginSecurityManifest.vulnerabilities is a '
+ + 'forced consequence rather than a name the ruling listed — it was the last authorable '
+ + 'referent of KernelSecurityVulnerability and could not outlive the def. Two neighbours '
+ + 'the ruling made CONDITIONAL are deliberately untouched here because the repository the '
+ + 'condition names, objectstack-ai/cloud, is not reachable from the session that executed '
+ + 'this: the marketplace "scanning" status stays exactly as it is — unremoved, and NOT '
+ + 'recorded as checked. Its two siblings were MEASURED rather than assumed, and the '
+ + 'record is corrected here: both were ALREADY GONE when that ruling was written. 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); see '
+ + 'incident-response-family-retired. And marketplace-admin.zod.ts was deleted outright '
+ + 'with the cloud subpath (#16526); see cloud-subpath-retired. Verified on this tree by '
+ + 'shape: both files return zero tree entries and no *.zod.ts names malware at all, '
+ + 'against a lit control where "scanning" still returns a live declaration in '
+ + 'marketplace.zod.ts. So the conditional question is ONE enum member wide, not three, '
+ + 'and the objectstack-ai/cloud producer grep it still owes is that much smaller. '
+ + '⚠️ The out-of-repo consumer population is NOT MEASURED. @objectstack/spec is published, '
+ + 'so this removal is breaking for consumers no download, dependent or source telemetry '
+ + 'was consulted for — accepted as an input to the ruling, exactly as that retirement states '
+ + 'of its own three exports, and not a reason to soften the removal. #15932, PR #15930 '
+ + '(for the deleted issue 14919), ADR-0049, ADR-0087.',
+ acceptanceCriteria:
+ 'No source imports KernelSecurityScanResult, KernelSecurityVulnerability or either '
+ + 'Schema from @objectstack/spec/kernel: both defs are absent from the built kernel '
+ + 'barrel and from api-surface/kernel.json, so a TypeScript consumer gets the refusal at '
+ + 'compile time at the import site rather than a missing runtime value. Authoring '
+ + 'PluginSecurityManifest.scanResults, PluginSecurityManifest.vulnerabilities or '
+ + 'PluginQualityMetrics.securityScan fails to compile (input type `never`) and fails to '
+ + 'parse with the tombstone prescription naming that key — verified by refusal pins that '
+ + 'assert the issue code, the path naming WHICH key was refused, and the prescription '
+ + 'text, plus a positive pin that the surrounding manifest still parses and grows no such '
+ + 'property. ⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: '
+ + 'nothing ever read any of these keys, so deleting one removes no check that was running. '
+ + 'A publisher who believed a declared scanResults entry gated anything was never getting '
+ + 'that gate; the remediation is to audit with a real tool, not to find a replacement key. '
+ + 'The surviving neighbours must still parse and still be exported — permissions, sandbox, '
+ + 'policy, codeSigning, certifications, securityContact and vulnerabilityDisclosure on the '
+ + 'manifest, testCoverage/documentationScore/codeQuality/conformanceTests on the quality '
+ + 'metrics, and the separately-declared SecurityScanResultSchema / '
+ + 'SecurityVulnerabilitySchema in kernel/plugin-security.zod.ts, which this change does '
+ + 'not touch.',
+ },
{
id: 'plugin-security-scanner-retired',
surface:
@@ -14734,6 +14825,74 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly>
// (`packages/core/src/health-monitor.ts`), never authored. See
// `kernel-plugin-health-report-durations-unit-in-key`.
'kernel/PluginHealthReport:metrics.uptime',
+ // #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65,
+ // 2026-09-07, maintainer verbatim 「同意」). `PluginQualityMetrics.securityScan` is
+ // the scan-result family's sibling on the plugin registry entry, named by the
+ // ruling alongside it: a last-scan date, per-severity vulnerability counts and a
+ // `passed` boolean, read by no scanner, registry, installer or UI. The census put
+ // every reference in `packages/spec/src/kernel/plugin-registry.test.ts` — the
+ // spec's own self-test and nothing else.
+ //
+ // It is the sharper half of the family for an author: `scanResults` published a
+ // report, but `securityScan.passed` published a VERDICT, so a plugin could ship
+ // `passed: true` with nothing at all behind it and a consumer reading the
+ // registry entry had no way to tell that from a real result.
+ //
+ // Tombstoned with `retiredKey()`: `PluginQualityMetricsSchema` is a plain
+ // `z.object`, so a bare deletion would strip an authored block in silence
+ // (ADR-0104). The key carried NO default of its own — the defaults inside it
+ // (`critical`/`high`/`medium`/`low = 0`, `passed = false`) fired only for an
+ // author who wrote the block — so `acceptRetiredDefaultResidue` is not owed:
+ // there is no value a released toolchain materialized into an artifact whose
+ // author never typed the key.
+ //
+ // No D2 conversion: a plugin registry entry is a published package artifact, not
+ // a stack collection member or a stored `sys_metadata` row. The ledger channel is
+ // `plugin-security-scan-result-surface-retired`.
+ 'kernel/PluginQualityMetrics:securityScan',
+ // #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65,
+ // 2026-09-07, maintainer verbatim 「同意」). `PluginSecurityManifest.scanResults`
+ // published an array of `KernelSecurityScanResult` on the authorable surface with
+ // zero authors and zero parsers: no `.parse`/`.safeParse` site existed anywhere
+ // against the scan-result schemas, so a publisher could declare a clean scan on a
+ // plugin manifest, be accepted, and get nothing — the declared-not-enforced shape
+ // Prime Directive #10 names, one layer out from the runtime scanner that PR
+ // #15930 removed for the same reason (that PR closed issue 14919, a number since
+ // deleted from the board and no longer resolving).
+ //
+ // Tombstoned with `retiredKey()`, not deleted: `PluginSecurityManifestSchema` is a
+ // plain `z.object`, not `.strict()`, so a bare deletion would strip an authored
+ // key in silence (ADR-0104) — swapping an inert declaration for an invisible one.
+ // The value type leaves this build entirely (`RETIRED_DEFS_BY_MAJOR[18]`,
+ // `kernel/KernelSecurityScanResult`).
+ //
+ // No D2 conversion: a security manifest is a package artifact a publisher ships,
+ // never a stack collection member and never a stored `sys_metadata` row, so the
+ // chain has no seam that would see one — the disposition the sibling
+ // `vulnerabilityDisclosure.responseTime` entry on this same schema already
+ // records. The prescription an author meets is the tombstone itself; the ledger
+ // channel is `plugin-security-scan-result-surface-retired`.
+ 'kernel/PluginSecurityManifest:scanResults',
+ // #15932 — ADR-0049 enforce-or-remove, decision batch #65.
+ // `PluginSecurityManifest.vulnerabilities` was an array of
+ // `KernelSecurityVulnerability` and is this retirement's FORCED CONSEQUENCE
+ // rather than a name the ruling listed: it was the last authorable referent of a
+ // def the ruling retires by name, so it cannot survive the def, and keeping the
+ // def alive only to carry it would be keeping the retired family alive under a
+ // second name. It is inert on its own terms too — nothing ever wrote the list and
+ // nothing ever read it, so declaring a known vulnerability against a plugin
+ // warned nobody and blocked no install.
+ //
+ // ⚠️ This is the ONE key outside the four names the #15932 dispatch fenced
+ // (`KernelSecurityScanResult`, `KernelSecurityVulnerability`,
+ // `PluginSecurityManifest.scanResults`, `PluginQualityMetrics.securityScan`), and
+ // it is reported as such on the card and in the landing PR rather than absorbed
+ // silently. It is not a neighbour retired by proximity — the fence's stated
+ // concern — it is a referent of a named retiree.
+ //
+ // Tombstoned with `retiredKey()` for the reason its `scanResults` sibling records
+ // (non-strict shape, ADR-0104 silent strip). No D2 conversion, same reasoning.
+ 'kernel/PluginSecurityManifest:vulnerabilities',
// #15678 (stack card 3/6 of #14478) — ruling B.
// `PluginSecurityManifest.vulnerabilityDisclosure.responseTime` said "Expected
// response time in hours" in prose and nothing else. Renamed to
@@ -17213,6 +17372,60 @@ export const RETIRED_DEFS_BY_MAJOR: Readonly>
// keeps emitting — see `18.kernel__PluginStartupResult__health.ts`. Route 3;
// the D3 semantic entry `startup-orchestrator-retired` carries the record.
'kernel/HealthStatus',
+ // #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65,
+ // 2026-09-07, maintainer verbatim 「同意」). `KernelSecurityScanResult` declared a
+ // complete scan report — timestamp, scanner name/version, a passed/failed/warning
+ // status, vulnerability and code-issue lists, dependency findings, license
+ // compliance and a six-number summary — and no layer ever emitted, stored, parsed
+ // or read one. Its only importer of any kind was `PluginSecurityScanner`, a
+ // type-only import from `packages/core/src/security/security-scanner.ts`, and
+ // PR #15930 deleted that file (it closed issue 14919, a number since deleted
+ // from the board and no longer resolving); the census after it landed put every remaining
+ // reference inside the declaring module itself, against a lit control (five hits
+ // for `PluginSecurityManifest` in the same file), so the zero is a reading.
+ //
+ // Whole-def retirement, not a tombstone: nothing parses this def, so there is no
+ // author to hand a prescription to and no `${defKey}:${name}` key leaving a live
+ // shape. `RETIRED_DEFS_BY_MAJOR[18]` plus the semantic entry
+ // `plugin-security-scan-result-surface-retired` ARE the declaration — the
+ // #11825 shape (its sibling in the pair this tree usually cites, issue 8715, is
+ // a number deleted from the board; 11825 is the half that still resolves).
+ // The two authorable carriers that pointed here,
+ // `PluginSecurityManifest.scanResults` and `.vulnerabilities`, are separately
+ // tombstoned and registered in `RETIRED_KEYS_BY_MAJOR[18]`.
+ //
+ // No D2 conversion: a plugin security manifest is a package artifact a publisher
+ // ships, never a stack collection member and never a stored `sys_metadata` row,
+ // so the conversion chain has no seam that would see one (the sibling
+ // `kernel/PluginSecurityManifest:vulnerabilityDisclosure.responseTime` entry
+ // records the same reasoning for the same schema).
+ 'kernel/KernelSecurityScanResult',
+ // #15932 — ADR-0049 enforce-or-remove (director seat, decision batch #65,
+ // 2026-09-07, maintainer verbatim 「同意」). The other half of the scan-result
+ // family: a CVE-shaped vulnerability record (severity, CVSS score, affected and
+ // fixed versions, exploit/patch availability, remediation, disclosure dates) that
+ // nothing ever constructed, validated or consulted. It reached this build through
+ // exactly three referents, all now gone — `KernelSecurityScanResult.vulnerabilities`
+ // and `.dependencyVulnerabilities[].vulnerability` (that def leaves in the same
+ // change) and `PluginSecurityManifest.vulnerabilities` (tombstoned). Retired
+ // together with `KernelSecurityScanResult` because declaring a vulnerability
+ // vocabulary with no scan to carry it reads as a capability, which is the
+ // ADR-0049 shape.
+ //
+ // Whole-def retirement for the reason its sibling entry records, and the same
+ // disposition: `RETIRED_DEFS_BY_MAJOR[18]` plus
+ // `plugin-security-scan-result-surface-retired`, no D2 conversion, no tombstone
+ // of its own. Its two authorable defaults —
+ // `KernelSecurityVulnerability:exploitAvailable = false` and
+ // `:patchAvailable = false` — leave `authorable-defaults/kernel.json` with the
+ // def; nothing ever parsed this schema, so no released toolchain ever
+ // materialized either value into an artifact and there is no residue to accept.
+ //
+ // ⛔ NOT in scope, and deliberately untouched: the unprefixed
+ // `SecurityVulnerabilitySchema` / `SecurityScanResultSchema` pair in the sibling
+ // module `kernel/plugin-security.zod.ts`. Those are separate defs with their own
+ // self-test and are outside this ruling.
+ 'kernel/KernelSecurityVulnerability',
// #13135 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-29 on
// #12057: retirement adopted, re-scope rejected; re-charter #13135 executes
// the widened surface). Part of the whole-module removal of