diff --git a/.changeset/manifest-contributes-dead-members-retired.md b/.changeset/manifest-contributes-dead-members-retired.md new file mode 100644 index 0000000000..cc284cc7a5 --- /dev/null +++ b/.changeset/manifest-contributes-dead-members-retired.md @@ -0,0 +1,60 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec): retire the nine dead members of the plugin-manifest `contributes` block — `events` / `menus` / `themes` / `translations` / `actions` / `drivers` / `fieldTypes` / `functions` / `commands` (#10724, ADR-0049 enforce-or-remove) + + + +**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep +launch-window convention ships it as `minor`; the migration prescription is +registered under protocol major 18, where `os migrate meta` users will look). + +The census behind it (#10627, controlled and re-verified at claim time across +objectstack, objectui and cloud) measured that the ENTIRE monorepo contains +exactly one non-test read of `manifest.contributes`, and it reads `kinds` +(`packages/objectql/src/engine.ts` → `registry.registerKind`). The other nine +members parsed, entered the manifest, and changed nothing — while published +material kept teaching them: `commands` documented Commander.js runtime +resolution the CLI dropped for oclif auto-discovery, `fieldTypes` advertised a +registration seam that has never existed, and `events` was decorative even for +its only in-repo author, which already subscribes imperatively. + +**What is refused:** authoring any of the nine keys. Each is a `retiredKey()` +tombstone (the `manifest.loading` precedent — neither `ManifestSchema` nor the +`contributes` object is `.strict()`, so a plain deletion would have silently +stripped the keys), so authoring one is a `tsc` error and a parse error +carrying the per-key prescription. + +**FROM → TO, per member** (each tombstone carries its own one-line fix): + +- `contributes.events` → subscribe in plugin code (`ctx.hook('kernel:ready', …)` + from `init`/`start`); delete the key. +- `contributes.menus` → app `navigation` / `manifest.navigationContributions` + (ADR-0029 D7); delete the key. +- `contributes.themes` → the stack-level `themes` metadata collection (an + unrelated `ThemeSchema` surface); delete the key. +- `contributes.translations` → the `translation` metadata type: + `defineTranslationBundle` in `defineStack({ translations })`; delete the key. +- `contributes.actions` → the stack `actions` collection or + `engine.registerAction`; delete the key. +- `contributes.drivers` → register a kernel service named `driver.*`; delete + the key. +- `contributes.fieldTypes` → nothing (no registration seam exists; the + vocabulary is the spec `FieldType` enum); delete the key. +- `contributes.functions` → `defineStack({ functions })`; delete the key. +- `contributes.commands` → oclif native plugin auto-discovery (an `oclif` + section in the plugin's own `package.json`; see `cli-extension.zod.ts`); + delete the key. + +**What stays:** `contributes.kinds` (the block's one live member) and +`contributes.routes` (an open enforce-or-remove fork, #10726 — deliberately +untouched here). Runtime behaviour is unchanged: nothing ever read the nine +members, so removing them removes no behaviour; a stored manifest still +carrying one degrades to a single `[metadata_spec_invalid]` log line at +registration rather than a boot failure. + +D3 semantic entry `plugin-manifest-contributes-dead-members-retired`; no D2 +conversion, because a package manifest is not a stack collection member +(`PLURAL_TO_SINGULAR` has no `packages`/`plugins` entry) and a conversion +would be a transform with no seam that ever runs. diff --git a/.changeset/manifest-kind-globs-retired.md b/.changeset/manifest-kind-globs-retired.md new file mode 100644 index 0000000000..6a5c7e31c1 --- /dev/null +++ b/.changeset/manifest-kind-globs-retired.md @@ -0,0 +1,39 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec): retire `contributes.kinds[].globs` — the declared file-type watch patterns nothing ever read (#11169, ADR-0049 enforce-or-remove; maintainer-ruled 2026-08-24) + + + +**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep +launch-window convention ships it as `minor`; the migration prescription is +registered under protocol major 18, where `os migrate meta` users will look). + +**Removed key:** `manifest.contributes.kinds[].globs`. + +The schema promised that declaring `globs` "enables the system to parse and +validate new file types" (its own example: a BI plugin handling +`*.report.ts`). The promise was never kept: real glob-driven artifact +discovery reads `filePatterns` off the metadata type registry — which +`contributes.kinds` does not extend, as `metadata-plugin.zod.ts` records +outright — so an authored `globs` was accepted, stored, served back through +`GET /metadata/kind`, and never consulted. Measured (PR #11168, re-verified +with positive control at claim): zero value reads anywhere; the only non-test +occurrences of the path were the schema declaration and two type positions. + +**FROM → TO:** `kinds: [{ id, globs: […], description? }]` → +`kinds: [{ id, description? }]` — delete the `globs` key; the kind's `id` and +`description` are unchanged and still register. The key is a `retiredKey()` +tombstone, so authoring it is a `tsc` error and a parse error carrying this +prescription. + +**What stays:** the `contributes.kinds` bucket itself and its `id` field +(live: engine → `registry.registerKind`, served via `GET /metadata/kind`). +File-type discovery remains single-channel on the metadata type registry's +`filePatterns`; if plugin-extensible discovery is ever wanted, it gets +designed against that registry, not revived here. The `registerKind` / +`getAllKinds` type positions drop `globs` (type-only; the parameter widens). + +D3 semantic entry `plugin-manifest-kind-globs-retired`; no D2 conversion (a +manifest is not a stack collection member — no seam would ever run it). diff --git a/content/docs/protocol/kernel/i18n-standard.mdx b/content/docs/protocol/kernel/i18n-standard.mdx index ef1bcb8775..713930d8db 100644 --- a/content/docs/protocol/kernel/i18n-standard.mdx +++ b/content/docs/protocol/kernel/i18n-standard.mdx @@ -590,44 +590,63 @@ context.i18n.formatNumber(smallNumber, { ## Plugin Integration -Plugins register translation bundles in their manifest: +Translations are **metadata**, declared on the stack — not manifest entries. +(The former `contributes.translations` manifest key was removed in v17 (#10724, +ADR-0049): no loader ever read its `{ locale, path }` entries, so a manifest +that still carries it now fails the parse with the upgrade prescription.) + +A package declares a translation bundle and registers it in its stack's +`translations` collection: {/* os:check */} ```typescript -// plugin.manifest.ts — a plugin manifest validated by `ManifestSchema` -// from `@objectstack/spec/kernel` (there is no `definePlugin()` helper). -// Translation files are registered under `contributes.translations` as -// { locale, path } entries (see packages/spec/src/kernel/manifest.zod.ts). -const manifest = { - id: 'com.mycompany.crm', - name: '@mycompany/crm', - version: '1.0.0', - - contributes: { - translations: [ - { locale: 'en', path: 'i18n/en/account.json' }, - { locale: 'de', path: 'i18n/de/account.json' }, - ], +// src/translations/crm.translation.ts +import { defineTranslationBundle } from '@objectstack/spec/system'; + +export const CrmTranslationBundle = defineTranslationBundle({ + en: { + objects: { + crm_account: { + label: 'Account', + pluralLabel: 'Accounts', + fields: { + name: { label: 'Account Name' }, + }, + }, + }, }, -}; - -export default manifest; + de: { + objects: { + crm_account: { + label: 'Konto', + pluralLabel: 'Konten', + fields: { + name: { label: 'Kontoname' }, + }, + }, + }, + }, +}); ``` -### Translation File Registration +```typescript +// objectstack.config.ts +import { defineStack } from '@objectstack/spec'; +import { CrmTranslationBundle } from './src/translations/crm.translation.js'; -``` -@mycompany/crm/ - i18n/ - en/ - account.json → Namespace: crm.account - contact.json → Namespace: crm.contact - de/ - account.json - contact.json +export default defineStack({ + manifest: { + id: 'com.mycompany.crm', + version: '1.0.0', + type: 'app', + name: 'CRM', + }, + translations: [CrmTranslationBundle], +}); ``` -**Namespace Convention:** `{pluginName}.{filename}` +The engine registers the collection as `translation` metadata (the governed +`translation` type), and the i18n pipeline serves it. ### Using Plugin Translations diff --git a/content/docs/references/kernel/manifest.mdx b/content/docs/references/kernel/manifest.mdx index db4abcdffd..aec996fedf 100644 --- a/content/docs/references/kernel/manifest.mdx +++ b/content/docs/references/kernel/manifest.mdx @@ -40,7 +40,7 @@ const result = ManifestSchema.parse(data); | **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | | **dependencies** | `Record` | optional | Package dependencies | | **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | -| **contributes** | `{ kinds?: object[]; events?: string[]; menus?: Record; themes?: object[]; … }` | optional | Platform contributions | +| **contributes** | `{ kinds?: object[]; routes?: object[] }` | optional | Platform contributions | | **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) | | **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | | **extensions** | `Record` | optional | Extension points and contributions | 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 81f80c1aa1..2fecf2f65e 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -261,7 +261,7 @@ directory rather than per file. | `cloud/` | 83 | | `identity/` | 32 | | `integration/` | 10 | -| `kernel/` | 296 | +| `kernel/` | 288 | | `qa/` | 6 | | `shared/` | 20 | | `system/` | 360 | diff --git a/packages/drivers/driver-memory/objectstack.config.ts b/packages/drivers/driver-memory/objectstack.config.ts index fffe69126e..0070b176d6 100644 --- a/packages/drivers/driver-memory/objectstack.config.ts +++ b/packages/drivers/driver-memory/objectstack.config.ts @@ -247,15 +247,10 @@ const MemoryDriverPlugin: ObjectStackManifest = { extensions: [], }, - contributes: { - drivers: [ - { - id: 'memory', - label: 'In-Memory Storage', - description: 'Stores data in memory (volatile, for testing/development)', - }, - ], - } + // `contributes.drivers` was retired (#10724, ADR-0049): the declaration drove + // nothing — this driver is wired by registering the `driver.memory` kernel + // service (the objectql plugin calls `registerDriver` on `driver.*` services), + // which is the enforced channel. }; export default MemoryDriverPlugin; diff --git a/packages/objectql/src/engine-kind-registration-log.test.ts b/packages/objectql/src/engine-kind-registration-log.test.ts index 58cc433c13..c8dede5d31 100644 --- a/packages/objectql/src/engine-kind-registration-log.test.ts +++ b/packages/objectql/src/engine-kind-registration-log.test.ts @@ -52,9 +52,9 @@ describe('[#10729] contributes.kinds registration logging', () => { engine.registerApp({ id: 'com.example.bi', contributes: { - // Exactly the schema's shape — and exactly its own documented example - // ("Registering a BI plugin to handle *.report.ts"). - kinds: [{ id: 'sys.bi.report', globs: ['**/*.report.ts'] }], + // Exactly the schema's shape: `{ id, description? }` (`globs` was + // retired unread, #11169). + kinds: [{ id: 'sys.bi.report', description: 'BI reports' }], }, }); @@ -73,7 +73,7 @@ describe('[#10729] contributes.kinds registration logging', () => { engine.registerApp({ id: 'com.example.bi', - contributes: { kinds: [{ id: 'sys.bi.report', globs: ['**/*.report.ts'] }] }, + contributes: { kinds: [{ id: 'sys.bi.report' }] }, }); // `registerKind` → `registerItem('kind', kind, 'id')`. The value in the log @@ -91,7 +91,6 @@ describe('[#10729] contributes.kinds registration logging', () => { contributes: { kinds: [{ id: 'sys.bi.report', - globs: ['**/*.report.ts'], // Neither key is declared by the schema. They are what the old line // reached for, so an author who copied an ancient example could put // them here — and the log must NOT start preferring them again. diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index 8363cf95eb..5a8b87de01 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -4606,7 +4606,8 @@ export class ObjectQL implements IObjectQLEngine { for (const kind of manifest.contributes.kinds) { this._registry.registerKind(kind); // [#10729] Name the kind by its declared `id`. `contributes.kinds` - // items are `{ id, globs, description? }` (`manifest.zod.ts`) and + // items are `{ id, description? }` (`manifest.zod.ts`; `globs` was + // retired unread, #11169) and // `registerKind` keys the item on `id` (`registerItem('kind', kind, 'id')`), // so `id` is BOTH the only identifying field the schema declares and the // exact key the item is stored under — a reader of this line can look the diff --git a/packages/objectql/src/registry.ts b/packages/objectql/src/registry.ts index 705ff36fbf..3b5f57d9e3 100644 --- a/packages/objectql/src/registry.ts +++ b/packages/objectql/src/registry.ts @@ -3745,11 +3745,14 @@ export class SchemaRegistry { // Kind Helpers // ========================================== - registerKind(kind: { id: string, globs: string[] }) { + // #11169: `globs` left the kind shape — nothing ever read the value here or + // downstream (the two type positions below were its only non-schema + // occurrences); a kind entry is `{ id, description? }`, keyed on `id`. + registerKind(kind: { id: string; description?: string }) { this.registerItem('kind', kind, 'id'); } - - getAllKinds(): { id: string, globs: string[] }[] { + + getAllKinds(): { id: string; description?: string }[] { return this.listItems('kind'); } diff --git a/packages/plugins/plugin-hono-server/objectstack.config.ts b/packages/plugins/plugin-hono-server/objectstack.config.ts index 9c58dbf200..b18d19d301 100644 --- a/packages/plugins/plugin-hono-server/objectstack.config.ts +++ b/packages/plugins/plugin-hono-server/objectstack.config.ts @@ -230,13 +230,9 @@ const HonoServerPlugin: ObjectStackManifest = { extensions: [], }, - contributes: { - // System Events - events: [ - 'kernel:ready', - 'kernel:listening', - ], - }, + // `contributes.events` was retired (#10724, ADR-0049): the declaration drove + // nothing — this plugin already subscribes to `kernel:ready` / `kernel:listening` + // imperatively in its own code, which is the enforced channel. }; export default HonoServerPlugin; diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index cda2781cbf..30eb190982 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -816,7 +816,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | validation | seeded 2026-08-01 (#4488). The ADR-0020 carrier: the evaluator honors active/events/priority/severity/type/condition/message (the zod header's "only reads type/condition/…" prose is STALE — trust the ledger). Dead 3 = label/description/tags, declared governance metadata, kept unmarked. Union walk boundary recorded: only base + `script` keys walked; per-variant keys are governed by the evaluator's tests, not ledger rows. **No longer a registered metadata kind** — #4509 retired it under ADR-0088 (a standalone rule had no object-binding key and every variant is `.strict()`, so it bound to nothing and gated no write; a state machine authored that way saved cleanly and did nothing). The rule VOCABULARY is untouched and fully live via `object.validations[]`, so the ledger keeps governing it through the gate's spec-only override, alongside `webhook` and `query`. The contrast with the two bridges in the same batch is the point: enforce-or-remove picked ENFORCE where the feature existed and only the wiring was missing, and REMOVE where the shape itself could not carry the feature | | api | seeded 2026-08-04 (#5271, part of #5206; PR #5312) — **not a metadata type until that same change made it one**, which is the row's point: governance and registration landed together, the treatment `datasource` did not get (#4487) and paid for with six inert keys found by hand. What #5206 measured before the fix: `api` was in neither `DEFAULT_METADATA_TYPE_REGISTRY` nor `BUILTIN_METADATA_TYPE_SCHEMAS`, so `saveMetaItem`'s `resolveOverlaySchema('api', …)` → `getMetadataTypeSchema('api')` returned `undefined` and took its own documented branch — an unregistered type is stored **unvalidated** — while `getMetaTypes()` could not enumerate the type at all, so Studio rendered neither list nor form. That issue names the shape precisely and it is the inverse of this ledger's usual one: **enforced but undeclared** (the matcher was already indexing these entries, #5089), where `dead` is declared-but-unenforced. The seeding pass classified 27 keys — live 25 / planned 2 / dead 0 — each cited `file:line` at the consumer layer that reads it: the MATCHER (`packages/metadata/src/endpoint-matcher.ts`) indexes `name`/`path`/`method`; the EXECUTOR (`packages/runtime/src/endpoint-executor.ts`) dispatches on `type` and reads `target`/`objectParams`; the POLICY chain (`packages/runtime/src/endpoint-policy.ts` + `security/inbound-rate-limit.ts`) enforces `authRequired`/`rateLimit`/`cacheTtl`; the MAPPING layer (`packages/runtime/src/api-mapping.ts`) applies `inputMapping`/`outputMapping`; and OpenAPI enrichment (`packages/rest/src/openapi-endpoints.ts`) emits `summary`/`description`. Timing was the reason it was cheap: #5040's E-series had built every one of those consumers and all of it was on main, so each key had a real evidence path rather than a promise. **Planned 2 = `inputMapping.transform` + `outputMapping.transform`, and `planned` rather than `dead` is load-bearing**: `dead` here means parsed with no consumer — a silent no-op — and these are the opposite, parsed and then LOUDLY REFUSED at publish (`endpoint-publish-gate.ts` mappingGate) and again at runtime, because no transformation-function registry exists anywhere in the platform. An author who writes one is told so and told what to do instead, so there is nothing for enforce-or-remove to chase; they stay in the vocabulary because admitting them needs a function registry **and** a sandbox ruling (#5040 §3.4), which is a design decision, not a key to quietly delete. Zero dead | | capability | seeded 2026-08-08 (#5961; PR #6540) — `CapabilityDeclarationSchema`, the DECLARATION side of ADR-0066 D1's three-way separation: packages DEFINE a capability, permission sets GRANT it via `systemPermissions`, resources REQUIRE it via `requiredPermissions`. **The gate's 12 and the seeding PR's 5 are the same measurement at two granularities** — PR #6540 call-graph-closed **5 authorable properties**, every one to a real reader in `packages/plugins/plugin-security/src/bootstrap-declared-capabilities.ts` (the one consumer that turns a declaration into a `sys_capability` row), all `live`, with no `PENDING_GOVERNANCE` debt recorded; the other 7 are the ADR-0010 protection-envelope keys the gate auto-classifies `live` and which carry `null` verdicts in the file, exactly as on `permission`/`position`. The same worked example as `api` above and PR #6540 says so in those words — **enforced but undeclared**, the mirror of the hole #5271 closed. What #5961 measured: absent from `DEFAULT_METADATA_TYPE_REGISTRY`, `BUILTIN_METADATA_TYPE_SCHEMAS` and `HAND_CRAFTED_SCHEMAS`, so `isRuntimeCreateAllowed()` took its no-static-entry fallback (permanently true) and `saveMetaItem` its no-schema branch — `PUT /api/v1/meta/capability/:name` accepted **arbitrary JSON** onto an authorization surface whose names `systemPermissions`/`requiredPermissions` resolve by string, while `/meta/types` synthesised a false `allowRuntimeCreate: true` descriptor Studio drew a raw-JSON create form from. #5870 did not open that path (the write gate reads the registry, not the item store); it only made the type visible in `getMetaTypes()`, and both the issue and this row say so to stop the next reader filing it as a regression. Landed as ruling A on ADR-0066 D1's own authority: `allowRuntimeCreate: false` **and** `allowOrgOverride: false`, the second self-judged inside the ruling's rationale and flagged for veto — a tenant overlay of a package declaration would lift `scope` from `org` to `platform`, which is the one field on this type that is an escalation rather than display. Its reverse verification is worth copying: deleting the registry entry gave 7 red / 3 green and measured something **sharper than predicted** — a garbage payload turned 422 rather than resolving, i.e. the schema binding is a real second line of defence behind the registry row, not a restatement of it; deleting the schema binding alone gave exactly 3 red. `packageId` is the one key that reads oddly: deliberately a FALLBACK, not the primary, since #5870 added `capabilities` to the ObjectQL stamped-collection list so `_packageId` now reaches a declaration and wins — it stays `live` because the fallback branch still decides materialization for any declaration arriving unstamped. Zero dead | -| manifest | seeded 2026-08-23 (#10728) — **not a metadata type and not a stack collection either**, which is the row's whole point. `ManifestSchema` (`packages/spec/src/kernel/manifest.zod.ts:132`) is what an author writes as `objectstack.config.ts` or a packaged manifest; it is parsed at `packages/objectql/src/registry.ts:2950` and by `os plugin build`, and it sat outside the ratchet's universe entirely — `GOVERNED` listed no `plugin`/`manifest`/`package`, `SPEC_ONLY_SCHEMAS` covered only webhook/query/validation/qa, `PENDING_GOVERNANCE` was empty so the gate reported itself **complete**, and `liveness/` had no file for it. A ratchet extended only to unregistered KINDS would not have reached it either: the retired-key entry `17.kernel__Manifest__loading.ts` records that `PLURAL_TO_SINGULAR` has no `packages`/`plugins` entry, so a manifest is never walked as a stack collection member. That blind spot was paid for twice, by hand and after the fact — `loading`'s ten inert keys (#4914, one of them `sandboxing`, which isolated nothing while looking like isolation) and the `contributes` census (#10627), which found exactly ONE reader of the 11-member block monorepo-wide. Dead 21 = the ten dead `contributes` members (`kinds` is the sole live one — `engine.ts:4504` → `registerKind`), the five `capabilities.*` and two `configuration.*` keys (all three containers have ZERO reads of the container itself, so no key beneath one can be read), plus `extensions`, `integrity`, `runtime`, and the tombstoned `loading` whose row must stay because `retiredKey()` keeps the key in the walked shape. **Two of those are security-shaped and neither is retired here**: `integrity` declares per-file digests the runtime is documented to re-verify at unpack while nothing computes or checks them, and `runtime` — the ADR-0025 §3.6 trust tier — is read only by two CLI lines that ECHO the value, with no `runtime === 'sandbox'` branch anywhere, even though `loading`'s own tombstone redirects upgrading authors to it as something "which [is] enforced". ⚠️ The `contributes` dispositions are recorded as **PENDING** on purpose: #10724 (tombstone the nine mechanically-dead members) is open and blocked on the cloud census, and #10726 (`contributes.routes`, ruled B) routes its removal through that card — nothing has merged, so no row here says retired. ⚠️ Cloud is unmeasured throughout, carried unchanged from #10627 (that repo could not be attached to the measuring session), so every `dead` above is an objectstack+objectui verdict and an enforce-or-remove worklist row, not a licence to delete | +| manifest | seeded 2026-08-23 (#10728) — **not a metadata type and not a stack collection either**, which is the row's whole point. `ManifestSchema` (`packages/spec/src/kernel/manifest.zod.ts:132`) is what an author writes as `objectstack.config.ts` or a packaged manifest; it is parsed at `packages/objectql/src/registry.ts:2950` and by `os plugin build`, and it sat outside the ratchet's universe entirely — `GOVERNED` listed no `plugin`/`manifest`/`package`, `SPEC_ONLY_SCHEMAS` covered only webhook/query/validation/qa, `PENDING_GOVERNANCE` was empty so the gate reported itself **complete**, and `liveness/` had no file for it. A ratchet extended only to unregistered KINDS would not have reached it either: the retired-key entry `17.kernel__Manifest__loading.ts` records that `PLURAL_TO_SINGULAR` has no `packages`/`plugins` entry, so a manifest is never walked as a stack collection member. That blind spot was paid for twice, by hand and after the fact — `loading`'s ten inert keys (#4914, one of them `sandboxing`, which isolated nothing while looking like isolation) and the `contributes` census (#10627), which found exactly ONE reader of the 11-member block monorepo-wide. Dead 21 = the ten dead `contributes` members (`kinds` is the sole live one — `engine.ts:4504` → `registerKind`), the five `capabilities.*` and two `configuration.*` keys (all three containers have ZERO reads of the container itself, so no key beneath one can be read), plus `extensions`, `integrity`, `runtime`, and the tombstoned `loading` whose row must stay because `retiredKey()` keeps the key in the walked shape. **Two of those are security-shaped and neither is retired here**: `integrity` declares per-file digests the runtime is documented to re-verify at unpack while nothing computes or checks them, and `runtime` — the ADR-0025 §3.6 trust tier — is read only by two CLI lines that ECHO the value, with no `runtime === 'sandbox'` branch anywhere, even though `loading`'s own tombstone redirects upgrading authors to it as something "which [is] enforced". The `contributes` dispositions have since MOVED: the cloud leg was measured CLEAN 2026-08-24 (#10812; cloud `origin/main` @ 5b5925a, zero `manifest.contributes` reads, controls held), and #10724 then executed — the nine mechanically-dead members are `retiredKey()` tombstones (D3 `plugin-manifest-contributes-dead-members-retired`), their rows staying because a tombstone keeps the key in the walked shape. #10726 (`contributes.routes`, ruled B) remains open and its row still reads PENDING. The remaining non-`contributes` `dead` rows keep their three-repo census verdicts as an enforce-or-remove worklist, not a licence to delete | The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every misleading entry carries `authorWarn` so authors hear about it at compile time diff --git a/packages/spec/liveness/manifest.json b/packages/spec/liveness/manifest.json index f15b6a6595..5b9fbc7bbe 100644 --- a/packages/spec/liveness/manifest.json +++ b/packages/spec/liveness/manifest.json @@ -1,6 +1,6 @@ { "type": "manifest", - "_note": "ManifestSchema (packages/spec/src/kernel/manifest.zod.ts:132) — the plugin / package manifest an author writes as `objectstack.config.ts` or a packaged `manifest.json`, parsed by `ManifestSchema.parse` at packages/objectql/src/registry.ts:2950 and by `os plugin build` / `os plugin publish`. GOVERNED VIA THE GATE'S SPEC-ONLY OVERRIDE (SPEC_ONLY_SCHEMAS), seeded 2026-08-23. WHY THE OVERRIDE AND NOT THE REGISTRY: the manifest is not a metadata KIND (absent from BUILTIN_METADATA_TYPE_SCHEMAS) and not a stack collection either — the retired-key entry packages/spec/src/migrations/entries/retired-keys/17.kernel__Manifest__loading.ts records that `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is never walked as a stack collection member. It is therefore the THIRD category the override has had to reach, after `query` (a request surface) and `qa` (a file surface): a ratchet rooted in the registry could not ask who reads any of it, and a ratchet extended to unregistered KINDS would not reach it either. Like `query` and `qa` there is no registry to fold it back onto — the override IS its governance. WHAT THAT COST BEFORE THIS FILE EXISTED, twice, both by hand and after the fact: `loading` carried ten inert keys, one of them (`sandboxing`) security-shaped — it isolated nothing while looking like isolation — retired at #4914; and the 11-member `contributes` block turned out to have exactly ONE reader monorepo-wide (#10627). No gate asked either question because the manifest was not in the denominator. MEASUREMENT PROVENANCE: the `contributes` children below are #10627's verdict table verbatim (measured on origin/main 299b85e9d), re-verified for line drift on 2026-08-23 — its cited read site moved from engine.ts:4499 to :4504. Every other row was measured for this seeding on 2026-08-23 against b9e9227e3, by per-key probe with controls: the same probe finds real reads of sibling keys (`.packaging` at cli/src/commands/plugin/build.ts:126, `.data` at runtime/src/app-plugin.ts:946, `.contributes` at objectql/src/engine.ts:4504), so a zero here is about the path, not the pattern. CROSS-REPO LEG: ../objectui walked for every `dead` row (0 property reads of these keys; control — `manifest.(id|name|namespace|version)` returns 46 hits there, so manifest reads are findable). ⚠️ CLOUD IS UNMEASURED, carried forward from #10627 unchanged: that repo could not be attached to the measuring session (`add_repo` → \"you don't have access to objectstack-ai/cloud\"), and the cloud control plane mirrors these shapes when it validates a published `.osplugin` (manifest.zod.ts:10-18). Every `dead` row below is therefore scoped to objectstack + objectui, and #10724 already carries the cloud leg as a precondition to executing any removal. WALK BOUNDARY, recorded rather than silently skipped: the gate classifies one level and this file drills six containers one more, so keys BELOW a drilled child (e.g. `contributes.kinds[].id` / `.globs` / `.description`, `navigationContributions[].items[]`) sit outside the walk; where they were measured in the same pass their verdicts are recorded in the child's `note` instead of being fanned out into rows the gate would not check. `data` is not drilled here at all — it embeds SeedSchema, which the governed `seed` type classifies in full, so it is a RESOLVED deferral in scripts/liveness/undrilled-containers.baseline.json rather than a duplicated set of rows. DISPOSITIONS ARE RECORDED AS PENDING WHERE THEY ARE PENDING: #10724 (tombstone the nine mechanically-dead `contributes` members) is OPEN and `pm:blocked` on the cloud census; #10726 (`contributes.routes`) is OPEN, recommendation B — removal rides #10724. No tombstone has merged for any of them, so no row below says `retired`; only `loading` does, because its tombstone really landed (#4914).", + "_note": "ManifestSchema (packages/spec/src/kernel/manifest.zod.ts:132) — the plugin / package manifest an author writes as `objectstack.config.ts` or a packaged `manifest.json`, parsed by `ManifestSchema.parse` at packages/objectql/src/registry.ts:2950 and by `os plugin build` / `os plugin publish`. GOVERNED VIA THE GATE'S SPEC-ONLY OVERRIDE (SPEC_ONLY_SCHEMAS), seeded 2026-08-23. WHY THE OVERRIDE AND NOT THE REGISTRY: the manifest is not a metadata KIND (absent from BUILTIN_METADATA_TYPE_SCHEMAS) and not a stack collection either — the retired-key entry packages/spec/src/migrations/entries/retired-keys/17.kernel__Manifest__loading.ts records that `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is never walked as a stack collection member. It is therefore the THIRD category the override has had to reach, after `query` (a request surface) and `qa` (a file surface): a ratchet rooted in the registry could not ask who reads any of it, and a ratchet extended to unregistered KINDS would not reach it either. Like `query` and `qa` there is no registry to fold it back onto — the override IS its governance. WHAT THAT COST BEFORE THIS FILE EXISTED, twice, both by hand and after the fact: `loading` carried ten inert keys, one of them (`sandboxing`) security-shaped — it isolated nothing while looking like isolation — retired at #4914; and the 11-member `contributes` block turned out to have exactly ONE reader monorepo-wide (#10627). No gate asked either question because the manifest was not in the denominator. MEASUREMENT PROVENANCE: the `contributes` children below are #10627's verdict table verbatim (measured on origin/main 299b85e9d), re-verified for line drift on 2026-08-25 at claim of #10724 — the cited read site now sits at engine.ts:4604-4606. Every other row was measured for this seeding on 2026-08-23 against b9e9227e3, by per-key probe with controls: the same probe finds real reads of sibling keys (`.packaging` at cli/src/commands/plugin/build.ts:126, `.data` at runtime/src/app-plugin.ts:946, `.contributes` at objectql/src/engine.ts:4504), so a zero here is about the path, not the pattern. CROSS-REPO LEG: ../objectui walked for every `dead` row (0 property reads of these keys; control — `manifest.(id|name|namespace|version)` returns 46 hits there, so manifest reads are findable). CLOUD LEG MEASURED CLEAN 2026-08-24 (#10812, discharging #10724's precondition): cloud `origin/main` @ 5b5925a has zero `manifest.contributes` reads (the single regex hit is an HTTP query parameter on the marketplace route; controls held — 15 manifest-property reads findable, member words present), completing the three-repo census at exactly one live read (engine.ts, member `kinds`). WALK BOUNDARY, recorded rather than silently skipped: the gate classifies one level and this file drills six containers one more, so keys BELOW a drilled child (e.g. `contributes.kinds[].id` / `.globs` / `.description`, `navigationContributions[].items[]`) sit outside the walk; where they were measured in the same pass their verdicts are recorded in the child's `note` instead of being fanned out into rows the gate would not check. `data` is not drilled here at all — it embeds SeedSchema, which the governed `seed` type classifies in full, so it is a RESOLVED deferral in scripts/liveness/undrilled-containers.baseline.json rather than a duplicated set of rows. DISPOSITIONS: #10724 EXECUTED — the nine mechanically-dead `contributes` members (events/menus/themes/translations/actions/drivers/fieldTypes/functions/commands) are retiredKey tombstones as of @objectstack/spec 17.x (D3 `plugin-manifest-contributes-dead-members-retired`); their rows below say so and STAY (tombstones keep the key in the drilled shape). #10726 (`contributes.routes`) remains OPEN — `routes` is deliberately untouched and its row still reads PENDING. `loading`'s tombstone landed earlier (#4914).", "props": { "id": { "status": "live", @@ -100,57 +100,57 @@ "kinds": { "status": "live", "evidence": "packages/objectql/src/engine.ts:4504-4518 → packages/objectql/src/registry.ts:3748 (`registerKind`)", - "verifiedAt": "2026-08-23", + "verifiedAt": "2026-08-25", "evidenceScope": "cross-repo", - "note": "THE ONLY LIVE MEMBER OF THIS BLOCK, and the entire reason the block is not uniformly dead: #10627 measured that the whole monorepo contains exactly one non-test read of `manifest.contributes` and it reads `kinds`. Line drift re-verified for this seeding — #10627 cited engine.ts:4499-4501 on 299b85e9d; on b9e9227e3 the same three lines are :4504-4506. BELOW THE WALK: `registerKind(kind: { id, globs })` (registry.ts:3748) reads `id` and `globs`; the item's third declared key `description` is not read. CAVEAT ON THE LIVE VERDICT, filed as #10729: the typed accessor `getAllKinds()` (registry.ts:3752) has zero callers, so what the registered kind is consumed FOR downstream is unclear — the registration itself is real, the consumption of the bucket is not established." + "note": "THE ONLY LIVE MEMBER OF THIS BLOCK, and the entire reason the block is not uniformly dead: #10627 measured that the whole monorepo contains exactly one non-test read of `manifest.contributes` and it reads `kinds`. Line drift re-verified for this seeding — #10627 cited engine.ts:4499-4501 on 299b85e9d; on b9e9227e3 the same three lines are :4504-4506. BELOW THE WALK: `globs` was RETIRED (#11169, maintainer ruling 2026-08-24, ADR-0087 D3 `plugin-manifest-kind-globs-retired`) — zero value reads anywhere (the only non-test occurrences were the schema declaration and the two type positions on `registerKind`/`getAllKinds`, which now drop it); the promised file-type discovery runs off the metadata type registry's `filePatterns`, which `contributes.kinds` does not extend. A kind entry is `{ id, description? }`; `registerKind` keys the item on `id`, and `description` is not read. CAVEAT ON THE LIVE VERDICT, filed as #10729: the typed accessor `getAllKinds()` (registry.ts:3752) has zero callers, so what the registered kind is consumed FOR downstream is unclear — the registration itself is real, the consumption of the bucket is not established." }, "events": { "status": "dead", - "verifiedAt": "2026-08-23", + "verifiedAt": "2026-08-25", "evidenceScope": "cross-repo", - "note": "#10627 verdict. Its only in-repo author — packages/plugins/plugin-hono-server/objectstack.config.ts:233, declaring `kernel:ready` / `kernel:listening` — subscribes imperatively in plugin code; the declaration drives nothing, which makes it decorative rather than merely unused. Disposition PENDING: #10724 (tombstone route) is OPEN and `pm:blocked` on the cloud census; no tombstone has merged." + "note": "RETIRED, and the row must STAY: `retiredKey()` keeps the key in the walked/drilled shape (a tombstone, not a strict removal; the `loading` precedent). Removed in @objectstack/spec 17.x (#10724, ADR-0049), ADR-0087 D3 entry `plugin-manifest-contributes-dead-members-retired`, retired-key entry `kernel/Manifest:contributes.events`. Prior verdict kept below as provenance: #10627 verdict. Its only in-repo author — packages/plugins/plugin-hono-server/objectstack.config.ts:233, declaring `kernel:ready` / `kernel:listening` — subscribes imperatively in plugin code; the declaration drives nothing, which makes it decorative rather than merely unused." }, "menus": { "status": "dead", - "verifiedAt": "2026-08-23", + "verifiedAt": "2026-08-25", "evidenceScope": "cross-repo", - "note": "#10627 verdict, and the tightest control in the set: the bare word `menus` has only three non-test hits monorepo-wide — this declaration, plus two alias maps that redirect the spelling to `navigation` (packages/spec/src/ui/app.zod.ts:1242, packages/spec/src/system/translation.zod.ts:428). The working surface is app `navigation` / `manifest.navigationContributions`. Disposition PENDING (#10724, open, `pm:blocked`)." + "note": "RETIRED, and the row must STAY: `retiredKey()` keeps the key in the walked/drilled shape (a tombstone, not a strict removal; the `loading` precedent). Removed in @objectstack/spec 17.x (#10724, ADR-0049), ADR-0087 D3 entry `plugin-manifest-contributes-dead-members-retired`, retired-key entry `kernel/Manifest:contributes.menus`. Prior verdict kept below as provenance: #10627 verdict, and the tightest control in the set: the bare word `menus` has only three non-test hits monorepo-wide — this declaration, plus two alias maps that redirect the spelling to `navigation` (packages/spec/src/ui/app.zod.ts:1242, packages/spec/src/system/translation.zod.ts:428). The working surface is app `navigation` / `manifest.navigationContributions`." }, "themes": { "status": "dead", - "verifiedAt": "2026-08-23", + "verifiedAt": "2026-08-25", "evidenceScope": "cross-repo", - "note": "#10627 verdict, re-verified there independently rather than inherited from the filer: the non-spec `themes` hits all reach the registry through TOP-LEVEL `manifest.themes` (METADATA_ARRAY_KEYS, packages/objectql/src/engine.ts:1884), never through `contributes.themes`, and the shapes differ ({id,label,path} here vs ThemeSchema there). Disposition PENDING (#10724, open, `pm:blocked`)." + "note": "RETIRED, and the row must STAY: `retiredKey()` keeps the key in the walked/drilled shape (a tombstone, not a strict removal; the `loading` precedent). Removed in @objectstack/spec 17.x (#10724, ADR-0049), ADR-0087 D3 entry `plugin-manifest-contributes-dead-members-retired`, retired-key entry `kernel/Manifest:contributes.themes`. Prior verdict kept below as provenance: #10627 verdict, re-verified there independently rather than inherited from the filer: the non-spec `themes` hits all reach the registry through TOP-LEVEL `manifest.themes` (METADATA_ARRAY_KEYS, packages/objectql/src/engine.ts:1884), never through `contributes.themes`, and the shapes differ ({id,label,path} here vs ThemeSchema there)." }, "translations": { "status": "dead", - "verifiedAt": "2026-08-23", + "verifiedAt": "2026-08-25", "evidenceScope": "cross-repo", - "note": "#10627 verdict — and documented as working: content/docs/protocol/kernel/i18n-standard.mdx carries an `os:check`-marked example stating translation files \"are registered under `contributes.translations`\". The live surface is the `translation` metadata type / stack `translations` collection (governed, packages/spec/liveness/translation.json). Disposition PENDING (#10724, open, `pm:blocked`); the doc correction rides that card." + "note": "RETIRED, and the row must STAY: `retiredKey()` keeps the key in the walked/drilled shape (a tombstone, not a strict removal; the `loading` precedent). Removed in @objectstack/spec 17.x (#10724, ADR-0049), ADR-0087 D3 entry `plugin-manifest-contributes-dead-members-retired`, retired-key entry `kernel/Manifest:contributes.translations`. Prior verdict kept below as provenance: #10627 verdict — and documented as working: content/docs/protocol/kernel/i18n-standard.mdx carries an `os:check`-marked example stating translation files \"are registered under `contributes.translations`\". The live surface is the `translation` metadata type / stack `translations` collection (governed, packages/spec/liveness/translation.json)." }, "actions": { "status": "dead", - "verifiedAt": "2026-08-23", + "verifiedAt": "2026-08-25", "evidenceScope": "cross-repo", - "note": "#10627 verdict. The live sibling is the stack `actions` collection (METADATA_ARRAY_KEYS, packages/objectql/src/engine.ts:1884) plus `engine.registerAction`. Disposition PENDING (#10724, open, `pm:blocked`)." + "note": "RETIRED, and the row must STAY: `retiredKey()` keeps the key in the walked/drilled shape (a tombstone, not a strict removal; the `loading` precedent). Removed in @objectstack/spec 17.x (#10724, ADR-0049), ADR-0087 D3 entry `plugin-manifest-contributes-dead-members-retired`, retired-key entry `kernel/Manifest:contributes.actions`. Prior verdict kept below as provenance: #10627 verdict. The live sibling is the stack `actions` collection (METADATA_ARRAY_KEYS, packages/objectql/src/engine.ts:1884) plus `engine.registerAction`." }, "drivers": { "status": "dead", - "verifiedAt": "2026-08-23", + "verifiedAt": "2026-08-25", "evidenceScope": "cross-repo", - "note": "#10627 verdict. Its only in-repo author — packages/drivers/driver-memory/objectstack.config.ts:250 — is registered by the SERVICE path (packages/objectql/src/plugin.ts:592, `registerDriver` for services named `driver.*`), not by its declaration: the second decorative declaration in the tree, alongside `events`. Disposition PENDING (#10724, open, `pm:blocked`)." + "note": "RETIRED, and the row must STAY: `retiredKey()` keeps the key in the walked/drilled shape (a tombstone, not a strict removal; the `loading` precedent). Removed in @objectstack/spec 17.x (#10724, ADR-0049), ADR-0087 D3 entry `plugin-manifest-contributes-dead-members-retired`, retired-key entry `kernel/Manifest:contributes.drivers`. Prior verdict kept below as provenance: #10627 verdict. Its only in-repo author — packages/drivers/driver-memory/objectstack.config.ts:250 — is registered by the SERVICE path (packages/objectql/src/plugin.ts:592, `registerDriver` for services named `driver.*`), not by its declaration: the second decorative declaration in the tree, alongside `events`." }, "fieldTypes": { "status": "dead", - "verifiedAt": "2026-08-23", + "verifiedAt": "2026-08-25", "evidenceScope": "cross-repo", - "note": "#10627 verdict, and the cleanest of the set: there is no `registerFieldType` seam anywhere — zero hits monorepo-wide — so this declares an extension point that does not exist rather than one that is merely unread. Disposition PENDING (#10724, open, `pm:blocked`)." + "note": "RETIRED, and the row must STAY: `retiredKey()` keeps the key in the walked/drilled shape (a tombstone, not a strict removal; the `loading` precedent). Removed in @objectstack/spec 17.x (#10724, ADR-0049), ADR-0087 D3 entry `plugin-manifest-contributes-dead-members-retired`, retired-key entry `kernel/Manifest:contributes.fieldTypes`. Prior verdict kept below as provenance: #10627 verdict, and the cleanest of the set: there is no `registerFieldType` seam anywhere — zero hits monorepo-wide — so this declares an extension point that does not exist rather than one that is merely unread." }, "functions": { "status": "dead", - "verifiedAt": "2026-08-23", + "verifiedAt": "2026-08-25", "evidenceScope": "cross-repo", - "note": "#10627 verdict. The live spelling is `defineStack({ functions })` → packages/objectql/src/hook-binder.ts:128-143. ⚠️ docs/adr/0088-metadata-kind-admission-and-retirement.md credits `contributes.functions` as a DELIVERED form of the `function` kind; that row is wrong and its correction rides #10724. Disposition PENDING (#10724, open, `pm:blocked`)." + "note": "RETIRED, and the row must STAY: `retiredKey()` keeps the key in the walked/drilled shape (a tombstone, not a strict removal; the `loading` precedent). Removed in @objectstack/spec 17.x (#10724, ADR-0049), ADR-0087 D3 entry `plugin-manifest-contributes-dead-members-retired`, retired-key entry `kernel/Manifest:contributes.functions`. Prior verdict kept below as provenance: #10627 verdict. The live spelling is `defineStack({ functions })` → packages/objectql/src/hook-binder.ts:128-143. ⚠️ docs/adr/0088-metadata-kind-admission-and-retirement.md credits `contributes.functions` as a DELIVERED form of the `function` kind; that row is wrong and its correction rides #10724." }, "routes": { "status": "dead", @@ -160,9 +160,9 @@ }, "commands": { "status": "dead", - "verifiedAt": "2026-08-23", + "verifiedAt": "2026-08-25", "evidenceScope": "cross-repo", - "note": "#10627 verdict, and already known-superseded in-tree: packages/spec/src/kernel/cli-extension.zod.ts:58-63 states \"The previous plugin model required `contributes.commands` … The `objectstack.config.ts` plugins array no longer determines CLI commands.\" Yet manifest.zod.ts:429-462 still documents Commander.js runtime resolution as current behaviour — two spec files contradicting each other, with the stale one on the authoring surface. Disposition PENDING (#10724, open, `pm:blocked`)." + "note": "RETIRED, and the row must STAY: `retiredKey()` keeps the key in the walked/drilled shape (a tombstone, not a strict removal; the `loading` precedent). Removed in @objectstack/spec 17.x (#10724, ADR-0049), ADR-0087 D3 entry `plugin-manifest-contributes-dead-members-retired`, retired-key entry `kernel/Manifest:contributes.commands`. Prior verdict kept below as provenance: #10627 verdict, and already known-superseded in-tree: packages/spec/src/kernel/cli-extension.zod.ts:58-63 states \"The previous plugin model required `contributes.commands` … The `objectstack.config.ts` plugins array no longer determines CLI commands.\" Yet manifest.zod.ts:429-462 still documents Commander.js runtime resolution as current behaviour — two spec files contradicting each other, with the stale one on the authoring surface." } } }, diff --git a/packages/spec/src/kernel/manifest.test.ts b/packages/spec/src/kernel/manifest.test.ts index d3b1b3c659..b35f341577 100644 --- a/packages/spec/src/kernel/manifest.test.ts +++ b/packages/spec/src/kernel/manifest.test.ts @@ -177,14 +177,14 @@ describe('ManifestSchema', () => { type: 'plugin', name: 'Business Intelligence', contributes: { + // `globs` retired (#11169) — a kind entry is `{ id, description? }`. kinds: [ { id: 'bi.dataset', - globs: ['**/*.dataset.json'] + description: 'BI dataset kind' }, { - id: 'bi.dashboard', - globs: ['**/*.bi-dash.json'] + id: 'bi.dashboard' } ] } @@ -193,49 +193,10 @@ describe('ManifestSchema', () => { expect(() => ManifestSchema.parse(biPlugin)).not.toThrow(); }); - it('should accept plugin with CLI command contributions', () => { - const marketplacePlugin: ObjectStackManifest = { - id: 'com.acme.marketplace', - version: '1.0.0', - type: 'plugin', - name: 'Marketplace CLI', - contributes: { - commands: [ - { - name: 'marketplace', - description: 'Manage marketplace applications', - module: './dist/cli.js', - }, - { - name: 'deploy', - description: 'Deploy to cloud', - }, - ], - }, - }; - - expect(() => ManifestSchema.parse(marketplacePlugin)).not.toThrow(); - const parsed = ManifestSchema.parse(marketplacePlugin); - expect(parsed.contributes!.commands).toHaveLength(2); - expect(parsed.contributes!.commands![0].name).toBe('marketplace'); - expect(parsed.contributes!.commands![1].module).toBeUndefined(); - }); - - it('should reject CLI command with invalid name format', () => { - const invalidPlugin = { - id: 'com.acme.bad', - version: '1.0.0', - type: 'plugin' as const, - name: 'Bad Plugin', - contributes: { - commands: [ - { name: 'Invalid_Name' }, - ], - }, - }; - - expect(() => ManifestSchema.parse(invalidPlugin)).toThrow(); - }); + // `contributes.commands` acceptance pins removed with the key (#10724): + // the CLI never resolved commands from the declaration (oclif + // auto-discovery is the enforced channel — `cli-extension.zod.ts`). The + // rejection is pinned with its eight retired siblings below. it('should accept authentication plugin manifest', () => { const authPlugin: ObjectStackManifest = { @@ -456,3 +417,95 @@ describe('ManifestSchema', () => { }); }); }); + +describe('contributes dead-member retirement (#10724, ADR-0049 — tombstoned, not deleted)', () => { + // Nine members had zero readers monorepo-wide (#10627's controlled census, + // completed on cloud 2026-08-24). `ManifestSchema` and the `contributes` + // object are NOT `.strict()`, so a plain deletion would have silently + // stripped the keys — `retiredKey()` is what makes each rejection carry the + // prescription, and the prescription is what these pins assert (the specific + // zod issue, never just "it threw"). + const base = { id: 'com.example.retired', version: '1.0.0', type: 'plugin', name: 'Retired' }; + const authored: Array<[member: string, value: unknown]> = [ + ['events', ['kernel:ready']], + ['menus', { toolbar: [{ id: 'm', label: 'M' }] }], + ['themes', [{ id: 't', label: 'T', path: './theme.css' }]], + ['translations', [{ locale: 'en', path: 'i18n/en.json' }]], + ['actions', [{ name: 'do_thing' }]], + ['drivers', [{ id: 'memory', label: 'In-Memory' }]], + ['fieldTypes', [{ name: 'vector', label: 'Vector' }]], + ['functions', [{ name: 'distance' }]], + ['commands', [{ name: 'marketplace' }]], + ]; + + it.each(authored)('REJECTS an authored `contributes.%s` with the prescription as the issue', (member, value) => { + const result = ManifestSchema.safeParse({ ...base, contributes: { [member]: value } }); + expect(result.success).toBe(false); + if (result.success) return; + // The SPECIFIC zod issue: located at the retired key, carrying the + // fully-qualified key, the removal record, and the imperative fix. + const issue = result.error.issues.find( + (i) => i.path[0] === 'contributes' && i.path[1] === member, + ); + expect(issue).toBeDefined(); + expect(issue!.message).toMatch( + new RegExp(`manifest\\.contributes\\.${member}.*removed in @objectstack/spec 17.*#10724.*Delete the key`, 's'), + ); + }); + + it('still parses the two survivors — `kinds` registers, `routes` is untouched (#10726 fork)', () => { + const parsed = ManifestSchema.parse({ + ...base, + contributes: { + kinds: [{ id: 'sys.bi.report', description: 'BI report kind' }], + routes: [{ prefix: '/api/v1/example', service: 'example' }], + }, + }); + expect(parsed.contributes!.kinds).toHaveLength(1); + expect(parsed.contributes!.routes).toHaveLength(1); + }); + + it('parses cleanly with the retired keys simply absent', () => { + const parsed = ManifestSchema.parse({ ...base, contributes: {} }); + expect(parsed.contributes).toEqual({}); + expect(parsed.contributes).not.toHaveProperty('commands'); + }); +}); + +describe('contributes.kinds[].globs retirement (#11169, ADR-0049 — maintainer-ruled 2026-08-24)', () => { + // The sub-field promised glob-driven file-type discovery that actually runs + // off the metadata type registry's `filePatterns` — which `contributes.kinds` + // does not extend — so an authored `globs` was stored, served back, and never + // consulted. The kinds item object is not `.strict()`, so the removal is a + // `retiredKey()` tombstone; the pin asserts the SPECIFIC zod issue. + const base = { id: 'com.example.kinds', version: '1.0.0', type: 'plugin', name: 'Kinds' }; + + it('REJECTS an authored kinds[].globs with the prescription as the issue', () => { + const result = ManifestSchema.safeParse({ + ...base, + contributes: { kinds: [{ id: 'sys.bi.report', globs: ['**/*.report.ts'] }] }, + }); + expect(result.success).toBe(false); + if (result.success) return; + const issue = result.error.issues.find( + (i) => + i.path[0] === 'contributes' && i.path[1] === 'kinds' && + i.path[2] === 0 && i.path[3] === 'globs', + ); + expect(issue).toBeDefined(); + expect(issue!.message).toMatch( + /manifest\.contributes\.kinds\[\]\.globs.*removed in @objectstack\/spec 17.*#11169.*filePatterns.*Delete the key/s, + ); + }); + + it('still parses and keeps a kind entry of `{ id, description? }` — the bucket and `id` are untouched', () => { + const parsed = ManifestSchema.parse({ + ...base, + contributes: { kinds: [{ id: 'sys.bi.report', description: 'BI report kind' }] }, + }); + expect(parsed.contributes!.kinds).toEqual([ + { id: 'sys.bi.report', description: 'BI report kind' }, + ]); + expect(parsed.contributes!.kinds![0]).not.toHaveProperty('globs'); + }); +}); diff --git a/packages/spec/src/kernel/manifest.zod.ts b/packages/spec/src/kernel/manifest.zod.ts index bbfe9d9be4..2513cd14b9 100644 --- a/packages/spec/src/kernel/manifest.zod.ts +++ b/packages/spec/src/kernel/manifest.zod.ts @@ -316,95 +316,124 @@ export const ManifestSchema = z.object({ /** * Contribution Points (VS Code Style). - * formalized way to extend the platform capabilities. + * + * NINE MEMBERS REMOVED in v17.x (#10724, ADR-0049 enforce-or-remove): + * `events`, `menus`, `themes`, `translations`, `actions`, `drivers`, + * `fieldTypes`, `functions`, `commands`. The census behind it (#10627, + * re-verified at claim time, three repos with control probes) measured + * exactly ONE non-test read of `manifest.contributes` in the entire + * monorepo — `packages/objectql/src/engine.ts` reading `kinds` — so every + * other member parsed, entered the manifest, and changed nothing. + * Tombstoned rather than deleted because this object is not `.strict()`: + * a plain deletion would silently strip the key, replacing an inert + * declaration with an invisible one (the `loading` precedent below). + * + * Survivors: `kinds` (live reader: engine → `registry.registerKind`) and + * `routes` (open enforce-or-remove fork #10726 — deliberately untouched + * by #10724; do not tombstone it here without its own ruling). */ contributes: z.object({ /** - * Register new Metadata Kinds (CRDs). - * Enables the system to parse and validate new file types. - * Example: Registering a BI plugin to handle *.report.ts + * Register new Metadata Kinds (identifiers). + * + * A declared kind is registered by the engine (`registry.registerKind`, + * keyed on `id`) and served back through `GET /metadata/kind`. It does + * NOT extend file-type discovery: artifact discovery globs `filePatterns` + * off the metadata type registry (`metadata-plugin.zod.ts`), which this + * declaration never fed — the former `globs` sub-field promised exactly + * that and was retired for it (#11169). */ kinds: z.array(z.object({ id: z.string().describe('The generic identifier of the kind (e.g., "sys.bi.report")'), - globs: z.array(z.string()).describe('File patterns to watch (e.g., ["**/*.report.ts"])'), + /** REMOVED (#11169) — discovery reads the metadata type registry's `filePatterns`, never this. */ + globs: retiredKey( + '`manifest.contributes.kinds[].globs` was removed in @objectstack/spec 17 (#11169, ' + + 'ADR-0049 enforce-or-remove) — it never had an effect: file-type discovery globs ' + + '`filePatterns` off the metadata type registry, which `contributes.kinds` does not ' + + 'extend (`metadata-plugin.zod.ts` states this outright), so the watch patterns ' + + 'declared here were stored, served back through `GET /metadata/kind`, and never ' + + "consulted. Delete the key; the kind's `id` (and optional `description`) still " + + 'register. If plugin-extensible file-type discovery is wanted, it must be designed ' + + 'against the `filePatterns` registry, not revived here.', + ), description: z.string().optional().describe('Description of what this kind represents'), - })).optional().describe('New Metadata Types to recognize'), - - /** - * Register System Hooks. - * Declares that this plugin listens to specific system events. - */ - events: z.array(z.string()).optional().describe('Events this plugin listens to'), - - /** - * Register UI Menus. - */ - menus: z.record(z.string(), z.array(z.object({ - id: z.string(), - label: z.string(), - command: z.string().optional(), - }))).optional().describe('UI Menu contributions'), - - /** - * Register Custom Themes. - */ - themes: z.array(z.object({ - id: z.string(), - label: z.string(), - path: z.string(), - })).optional().describe('Theme contributions'), - - /** - * Register Translations. - * Path to translation files (e.g. "locales/en.json"). - */ - translations: z.array(z.object({ - locale: z.string(), - path: z.string(), - })).optional().describe('Translation resources'), - - /** - * Register Server Actions. - * Invocable functions exposed to Flows or API. - */ - actions: z.array(z.object({ - name: z.string().describe('Unique action name'), - label: z.string().optional(), - description: z.string().optional(), - input: z.unknown().optional().describe('Input validation schema'), - output: z.unknown().optional().describe('Output schema'), - })).optional().describe('Exposed server actions'), - - /** - * Register Storage Drivers. - * Enables connecting to new types of datasources. - */ - drivers: z.array(z.object({ - id: z.string().describe('Driver unique identifier (e.g. "postgres", "mongodb")'), - label: z.string().describe('Human readable name'), - description: z.string().optional(), - })).optional().describe('Driver contributions'), - - /** - * Register Custom Field Types. - * Extends the data model with new widget types. - */ - fieldTypes: z.array(z.object({ - name: z.string().describe('Unique field type name (e.g. "vector")'), - label: z.string().describe('Display label'), - description: z.string().optional(), - })).optional().describe('Field Type contributions'), - - /** - * Register Custom Query Operators/Functions. - * Extends ObjectQL with new functions (e.g. distance()). - */ - functions: z.array(z.object({ - name: z.string().describe('Function name (e.g. "distance")'), - description: z.string().optional(), - args: z.array(z.string()).optional().describe('Argument types'), - returnType: z.string().optional(), - })).optional().describe('Query Function contributions'), + })).optional().describe('Metadata kind identifiers this package registers'), + + /** REMOVED (#10724) — the declaration drove nothing; subscribe in plugin code. */ + events: retiredKey( + '`manifest.contributes.events` was removed in @objectstack/spec 17 (#10724, ' + + 'ADR-0049 enforce-or-remove) — nothing ever read the list: its only in-repo ' + + 'author already subscribed imperatively in plugin code, so the declaration was ' + + 'decorative. Delete the key. Subscribe to system events in the plugin itself — ' + + "`ctx.hook('kernel:ready', …)` (or the events service) from `init`/`start` is " + + 'the enforced channel; record lifecycle hooks register on the data engine.', + ), + + /** REMOVED (#10724) — use app `navigation` / `manifest.navigationContributions`. */ + menus: retiredKey( + '`manifest.contributes.menus` was removed in @objectstack/spec 17 (#10724, ' + + 'ADR-0049 enforce-or-remove) — no renderer ever read it; two alias maps already ' + + 'redirected this spelling to `navigation`. Delete the key. Declare navigation in ' + + "the app's `navigation` tree, or inject items into another package's app via " + + '`manifest.navigationContributions` (ADR-0029 D7), which the engine registers.', + ), + + /** REMOVED (#10724) — this `{id,label,path}` shape had no reader anywhere. */ + themes: retiredKey( + '`manifest.contributes.themes` was removed in @objectstack/spec 17 (#10724, ' + + 'ADR-0049 enforce-or-remove) — it never had an effect: theme registration reaches ' + + 'the registry only through the stack-level `themes` collection (a ' + + '`ThemeSchema` surface, unrelated to this `{ id, label, path }` shape), never ' + + 'through `contributes.themes`. Delete the key; declare themes in the stack ' + + '`themes` collection instead.', + ), + + /** REMOVED (#10724) — use the `translation` metadata type / stack `translations`. */ + translations: retiredKey( + '`manifest.contributes.translations` was removed in @objectstack/spec 17 ' + + '(#10724, ADR-0049 enforce-or-remove) — no loader ever read these `{ locale, ' + + 'path }` entries; authoring them registered no translations. Delete the key. ' + + 'Declare translations as `translation` metadata: `defineTranslationBundle({ … })` ' + + "in the stack's `translations` collection (`defineStack({ translations: […] })`), " + + 'which the engine registers and the i18n pipeline serves.', + ), + + /** REMOVED (#10724) — use the stack `actions` collection / `registerAction`. */ + actions: retiredKey( + '`manifest.contributes.actions` was removed in @objectstack/spec 17 (#10724, ' + + 'ADR-0049 enforce-or-remove) — nothing ever read it; actions declared here were ' + + 'never invocable. Delete the key. Declare actions in the stack `actions` ' + + 'collection (registered by the engine) or register imperatively via ' + + '`engine.registerAction`.', + ), + + /** REMOVED (#10724) — a driver is a `driver.*` kernel service, not a declaration. */ + drivers: retiredKey( + '`manifest.contributes.drivers` was removed in @objectstack/spec 17 (#10724, ' + + 'ADR-0049 enforce-or-remove) — it never had an effect: a storage driver is wired ' + + 'by registering a kernel SERVICE named `driver.*` (the objectql plugin picks it ' + + 'up and calls `registerDriver`), and its only in-repo author was registered that ' + + 'way, not by this declaration. Delete the key.', + ), + + /** REMOVED (#10724) — no field-type registration seam exists. */ + fieldTypes: retiredKey( + '`manifest.contributes.fieldTypes` was removed in @objectstack/spec 17 (#10724, ' + + 'ADR-0049 enforce-or-remove) — there is no `registerFieldType` seam anywhere: ' + + 'the declaration advertised an extension point the platform does not have, so ' + + 'authoring it configured nothing. Delete the key. The field-type vocabulary is ' + + 'the spec `FieldType` enum; extending it is a spec change, not a manifest ' + + 'declaration.', + ), + + /** REMOVED (#10724) — use `defineStack({ functions })` → `registerFunction`. */ + functions: retiredKey( + '`manifest.contributes.functions` was removed in @objectstack/spec 17 (#10724, ' + + 'ADR-0049 enforce-or-remove) — nothing ever read it; ObjectQL functions declared ' + + 'here were never registered. Delete the key. Declare functions on the stack ' + + '(`defineStack({ functions: […] })`), which the hook binder registers via ' + + '`engine.registerFunction`.', + ), /** * Register API Route Namespaces. @@ -427,39 +456,20 @@ export const ManifestSchema = z.object({ })).optional().describe('API route contributions to HttpDispatcher'), /** - * Register CLI Commands. - * Allows plugins to extend the ObjectStack CLI with custom commands. - * Each command entry declares metadata; the actual Commander.js command - * is resolved at runtime by importing the plugin's module. - * - * The plugin package must export a `commands` array of Commander.js `Command` instances - * from its main entry point or from the path specified in `module`. - * - * @example - * ```yaml - * commands: - * - name: marketplace - * description: "Manage marketplace apps" - * module: "./cli" # optional, defaults to package main - * - name: deploy - * description: "Deploy to cloud" - * ``` - */ - commands: z.array(z.object({ - /** CLI command name (e.g., "marketplace", "deploy"). Must be a valid CLI identifier. */ - name: z.string() - .regex(/^[a-z][a-z0-9-]*$/, 'Command name must be lowercase alphanumeric with hyphens') - .describe('CLI command name'), - /** Brief description shown in `os --help` */ - description: z.string().optional().describe('Command description for help text'), - /** - * Optional module path (relative to package root) that exports the Commander.js commands. - * If omitted, the CLI will import from the package's main entry point. - * The module must export a `commands` array of Commander.js `Command` instances, - * or a single `Command` instance as default export. - */ - module: z.string().optional().describe('Module path exporting Commander.js commands'), - })).optional().describe('CLI command contributions'), + * REMOVED (#10724) — CLI commands are oclif-auto-discovered, never resolved + * from this declaration. The JSDoc that used to sit here described + * Commander.js runtime resolution as current behaviour; that contradicted + * `cli-extension.zod.ts`, which records the oclif migration: "The + * `objectstack.config.ts` plugins array no longer determines CLI commands." + */ + commands: retiredKey( + '`manifest.contributes.commands` was removed in @objectstack/spec 17 (#10724, ' + + 'ADR-0049 enforce-or-remove) — the CLI never resolved commands from this ' + + 'declaration: commands are auto-discovered through oclif\'s native plugin system ' + + "(the plugin package declares an `oclif` section in its own `package.json`; see " + + '`cli-extension.zod.ts`), and the `objectstack.config.ts` plugins array no longer ' + + 'determines CLI commands. Delete the key.', + ), }).optional().describe('Platform contributions'), /** diff --git a/packages/spec/src/kernel/metadata-plugin.zod.ts b/packages/spec/src/kernel/metadata-plugin.zod.ts index 5215b3cb3e..f9e81f7228 100644 --- a/packages/spec/src/kernel/metadata-plugin.zod.ts +++ b/packages/spec/src/kernel/metadata-plugin.zod.ts @@ -116,9 +116,10 @@ export const MetadataTypeSchema = lazySchema(() => z.enum([ 'external_catalog', // Cached remote schema snapshot for federated datasources (ADR-0015) — RUNTIME-CREATED by the Sync wizard (ADR-0062/0088); packages never ship one 'translation', // i18n resources (TranslationSchema) // ADR-0088: `router`/`function`/`service` are NOT metadata kinds — they are - // code contributions: plugin `contributes.routes` + declarative `apis:` - // (router), `defineStack({ functions })` + `contributes.functions` - // (function), and the plugin/service registry itself (service). + // code contributions: declarative `apis:` (router — `contributes.routes` + // parses but serves nothing, #10726), `defineStack({ functions })` + // (function — `contributes.functions` was retired unread, #10724), and the + // plugin/service registry itself (service). // // [#5271, part of #5206] `api` is the ONE declarative endpoint ITEM kind, and // it is NOT a reversal of the `router` retirement above: `router` was retired diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.actions.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.actions.ts new file mode 100644 index 0000000000..93aa718ce0 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.actions.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` +// block; one of NINE members tombstoned together. Census, registration major, +// and the why-no-D2-conversion reasoning are recorded once in the sibling +// entry `kernel/Manifest:contributes.events` (this family) and in +// `kernel/Manifest:loading` (the precedent); the D3 semantic entry is +// `plugin-manifest-contributes-dead-members-retired`. +// +// `actions` declared invocable server actions nothing ever registered. The +// working surfaces are the stack `actions` collection (METADATA_ARRAY_KEYS, +// registered by the engine) and `engine.registerAction`. +export const entry = 'kernel/Manifest:contributes.actions'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.commands.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.commands.ts new file mode 100644 index 0000000000..6607419263 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.commands.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` +// block; one of NINE members tombstoned together. Census, registration major, +// and the why-no-D2-conversion reasoning are recorded once in the sibling +// entry `kernel/Manifest:contributes.events` (this family) and in +// `kernel/Manifest:loading` (the precedent); the D3 semantic entry is +// `plugin-manifest-contributes-dead-members-retired`. +// +// `commands` documented Commander.js runtime resolution as current behaviour; +// the CLI never resolved commands from it. Commands are auto-discovered via +// oclif's native plugin system (`cli-extension.zod.ts` records the migration: +// "The `objectstack.config.ts` plugins array no longer determines CLI +// commands"). +export const entry = 'kernel/Manifest:contributes.commands'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.drivers.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.drivers.ts new file mode 100644 index 0000000000..1c6f2cff3c --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.drivers.ts @@ -0,0 +1,14 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` +// block; one of NINE members tombstoned together. Census, registration major, +// and the why-no-D2-conversion reasoning are recorded once in the sibling +// entry `kernel/Manifest:contributes.events` (this family) and in +// `kernel/Manifest:loading` (the precedent); the D3 semantic entry is +// `plugin-manifest-contributes-dead-members-retired`. +// +// `drivers` declared storage drivers nothing ever read. A driver is wired by +// registering a kernel SERVICE named `driver.*` (objectql's plugin calls +// `registerDriver` on it); the only in-repo author (driver-memory) was +// registered that way, not by its declaration. +export const entry = 'kernel/Manifest:contributes.drivers'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.events.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.events.ts new file mode 100644 index 0000000000..b202e345aa --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.events.ts @@ -0,0 +1,24 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` +// block (triage graded 2026-08-21; cloud leg measured clean 2026-08-24). One +// of NINE members tombstoned together: #10627 measured exactly ONE non-test +// read of `manifest.contributes` monorepo-wide (engine.ts, member `kinds`), +// with controls, re-verified across objectstack + objectui + cloud at claim +// time. `events` in particular was decorative twice over: its only in-repo +// author (plugin-hono-server) already subscribed to the same events +// imperatively in plugin code, so the declaration drove nothing even for the +// one package that wrote it. +// +// Registered under 18, not 17: v17.0.0 was cut before this landed, so the +// tombstone ships on the 17.x line (launch-window convention) and the +// prescription lives at the major boundary where `migrate meta` users look. +// +// Registered here but NOT in `src/conversions/registry.ts`, for the reason +// `kernel/Manifest:loading` gives: a package manifest is not a stack +// collection member (`PLURAL_TO_SINGULAR` has no `packages` / `plugins` +// entry), so a D2 conversion would be a transform with no seam that ever +// runs. The prescription reaches authors through the tombstone at +// `os plugin build` → `ManifestSchema.safeParse` and through the D3 semantic +// entry `plugin-manifest-contributes-dead-members-retired`. +export const entry = 'kernel/Manifest:contributes.events'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.fieldTypes.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.fieldTypes.ts new file mode 100644 index 0000000000..238cce21fa --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.fieldTypes.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` +// block; one of NINE members tombstoned together. Census, registration major, +// and the why-no-D2-conversion reasoning are recorded once in the sibling +// entry `kernel/Manifest:contributes.events` (this family) and in +// `kernel/Manifest:loading` (the precedent); the D3 semantic entry is +// `plugin-manifest-contributes-dead-members-retired`. +// +// `fieldTypes` advertised an extension point the platform does not have: +// there is no `registerFieldType` seam anywhere — zero hits monorepo-wide. +// The field-type vocabulary is the spec `FieldType` enum. +export const entry = 'kernel/Manifest:contributes.fieldTypes'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.functions.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.functions.ts new file mode 100644 index 0000000000..b5661b1f51 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.functions.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` +// block; one of NINE members tombstoned together. Census, registration major, +// and the why-no-D2-conversion reasoning are recorded once in the sibling +// entry `kernel/Manifest:contributes.events` (this family) and in +// `kernel/Manifest:loading` (the precedent); the D3 semantic entry is +// `plugin-manifest-contributes-dead-members-retired`. +// +// `functions` declared ObjectQL functions nothing ever registered from here. +// The working surface is `defineStack({ functions })` → hook-binder → +// `engine.registerFunction`. +export const entry = 'kernel/Manifest:contributes.functions'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.kinds.globs.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.kinds.globs.ts new file mode 100644 index 0000000000..f04c496033 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.kinds.globs.ts @@ -0,0 +1,28 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #11169 — ADR-0049 enforce-or-remove on `contributes.kinds[].globs` +// (maintainer ruling 2026-08-24, 「接受你的建议。」: remove via the full +// ceremony). The sub-field promised glob-driven file-type discovery the +// platform performs somewhere else: real artifact discovery globs +// `filePatterns` off the metadata type registry, and +// `metadata-plugin.zod.ts` states outright that `contributes.kinds` does not +// extend it. Measured (PR #11168, re-run with positive control at claim +// time): zero consumers — the only non-test occurrences of the path are the +// schema declaration and two type positions (`registerKind`'s parameter, +// `getAllKinds`' return), and nothing reads the value — so an authored +// `globs` was stored, served back through `GET /metadata/kind`, and never +// consulted. The `kind` bucket itself and its `id` are NOT touched: the +// bucket is live (engine → `registerKind`) and reachable via the generic +// `GET /metadata/:type` passthrough. +// +// Registered under 18, not 17: v17.0.0 was cut before this landed, so the +// tombstone ships on the 17.x line (launch-window convention) and the +// prescription lives at the major boundary where `migrate meta` users look. +// +// Registered here but NOT in `src/conversions/registry.ts`, for the reason +// `kernel/Manifest:loading` gives: a package manifest is not a stack +// collection member, so a D2 conversion would be a transform with no seam +// that ever runs. The prescription reaches authors through the tombstone at +// `os plugin build` → `ManifestSchema.safeParse` and through the D3 semantic +// entry `plugin-manifest-kind-globs-retired`. +export const entry = 'kernel/Manifest:contributes.kinds.globs'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.menus.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.menus.ts new file mode 100644 index 0000000000..6f26c166fb --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.menus.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` +// block; one of NINE members tombstoned together. Census, registration major, +// and the why-no-D2-conversion reasoning are recorded once in the sibling +// entry `kernel/Manifest:contributes.events` (this family) and in +// `kernel/Manifest:loading` (the precedent); the D3 semantic entry is +// `plugin-manifest-contributes-dead-members-retired`. +// +// `menus` had the tightest control in the census: the bare word has only three +// non-test hits monorepo-wide — this declaration plus two alias maps that +// redirect the spelling to `navigation`. The working surface is app +// `navigation` / `manifest.navigationContributions` (ADR-0029 D7), which the +// engine registers. +export const entry = 'kernel/Manifest:contributes.menus'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.themes.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.themes.ts new file mode 100644 index 0000000000..9da485e3c5 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.themes.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` +// block; one of NINE members tombstoned together. Census, registration major, +// and the why-no-D2-conversion reasoning are recorded once in the sibling +// entry `kernel/Manifest:contributes.events` (this family) and in +// `kernel/Manifest:loading` (the precedent); the D3 semantic entry is +// `plugin-manifest-contributes-dead-members-retired`. +// +// `themes` here was a `{ id, label, path }` shape with no reader anywhere. It +// is UNRELATED to the stack-level `themes` collection (a `ThemeSchema` +// surface, itself retired as a carrier by `stack-themes-carrier-retired`): +// stack-level theme hits reach the registry through top-level metadata +// collections, never through `contributes.themes`. +export const entry = 'kernel/Manifest:contributes.themes'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.translations.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.translations.ts new file mode 100644 index 0000000000..bc9e8a79dd --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.translations.ts @@ -0,0 +1,14 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` +// block; one of NINE members tombstoned together. Census, registration major, +// and the why-no-D2-conversion reasoning are recorded once in the sibling +// entry `kernel/Manifest:contributes.events` (this family) and in +// `kernel/Manifest:loading` (the precedent); the D3 semantic entry is +// `plugin-manifest-contributes-dead-members-retired`. +// +// `translations` promised `{ locale, path }` file registration no loader ever +// performed. The working surface is the `translation` metadata type — stack +// `translations` collection (`defineTranslationBundle`), governed by +// `packages/spec/liveness/translation.json`. +export const entry = 'kernel/Manifest:contributes.translations'; diff --git a/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-contributes-dead-members-retired.ts b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-contributes-dead-members-retired.ts new file mode 100644 index 0000000000..a171aa9118 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-contributes-dead-members-retired.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +export const entry: SemanticMigration = { + id: 'plugin-manifest-contributes-dead-members-retired', + surface: + 'manifest.contributes.events / manifest.contributes.menus / manifest.contributes.themes / ' + + 'manifest.contributes.translations / manifest.contributes.actions / ' + + 'manifest.contributes.drivers / manifest.contributes.fieldTypes / ' + + 'manifest.contributes.functions / manifest.contributes.commands (nine of the block\'s ' + + 'eleven members; `kinds` and `routes` are NOT part of this retirement)', + replacement: + 'delete the keys — each capability already has its one enforced channel: `events` → ' + + "subscribe imperatively in plugin code (`ctx.hook('kernel:ready', …)` from `init`/`start`); " + + '`menus` → the app `navigation` tree or `manifest.navigationContributions` (ADR-0029 D7); ' + + '`themes` → the stack-level `themes` metadata collection (an unrelated `ThemeSchema` ' + + 'surface); `translations` → the `translation` metadata type, authored with ' + + '`defineTranslationBundle` in `defineStack({ translations })`; `actions` → the stack ' + + '`actions` collection or `engine.registerAction`; `drivers` → register a kernel service ' + + 'named `driver.*` (objectql calls `registerDriver` on it); `fieldTypes` → nothing (no ' + + 'registration seam exists; the vocabulary is the spec `FieldType` enum); `functions` → ' + + '`defineStack({ functions })` → `engine.registerFunction`; `commands` → oclif native ' + + "plugin auto-discovery (an `oclif` section in the plugin's own `package.json`; see " + + '`cli-extension.zod.ts`)', + reason: + 'ADR-0049 enforce-or-remove; #10724 (triage graded 2026-08-21, cloud precondition ' + + 'discharged 2026-08-24). #10627 measured, monorepo-wide and non-test with control probes, ' + + 'that the ENTIRE monorepo contains exactly one read of `manifest.contributes` — ' + + '`packages/objectql/src/engine.ts`, member `kinds` — so all nine members above parsed, ' + + 'entered the manifest, and changed nothing. The census stands on three repos: objectstack ' + + '(re-verified on current main at claim time), objectui (0 property reads; control: 63 ' + + 'files carry the bare word), and cloud (measured clean 2026-08-24 at `5b5925a`: zero ' + + '`manifest.contributes` reads, controls held). Several members were actively misleading: ' + + '`events` was authored in-repo by a plugin that already subscribes imperatively; ' + + '`commands` documented Commander.js resolution the CLI dropped for oclif auto-discovery; ' + + '`fieldTypes` advertised a registration seam that has never existed. ' + + 'Why D3 semantic and not a D2 conversion: the conversion chain walks a normalized STACK ' + + 'and `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a ' + + 'stack collection member and a conversion would be a transform with no seam that ever ' + + 'runs (the `kernel/Manifest:loading` precedent, recorded verbatim in its retired-key ' + + 'entry).', + acceptanceCriteria: + 'No `objectstack.config.ts` manifest and no packaged `manifest.json` authors any of the ' + + 'nine members. The enforced channel is the one place a manifest is parsed with an author ' + + 'present: `os plugin build` runs `ManifestSchema.safeParse` and exits non-zero printing ' + + 'the per-key tombstone prescription; TypeScript authors fail earlier still (each key is ' + + 'typed `never`). `contributes.kinds` keeps parsing and registering ' + + '(`registry.registerKind`), and `contributes.routes` is untouched pending its own fork ' + + '(#10726). ⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: ' + + 'nothing ever read the nine members, so removing them removes no behaviour. A package ' + + 'ALREADY INSTALLED whose stored manifest carries one degrades to a single ' + + '`[metadata_spec_invalid]` log line at registration (the registry\'s `validate()` is a ' + + 'diagnostic, not a gate) rather than a boot failure; clear it by deleting the key from ' + + 'the source manifest and reinstalling.', +}; diff --git a/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-kind-globs-retired.ts b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-kind-globs-retired.ts new file mode 100644 index 0000000000..7a1b954ee5 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-kind-globs-retired.ts @@ -0,0 +1,44 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +export const entry: SemanticMigration = { + id: 'plugin-manifest-kind-globs-retired', + surface: 'manifest.contributes.kinds[].globs (the `kind` bucket itself and its `id` are untouched)', + replacement: + 'delete the key — a kind entry is `{ id, description? }`. File-type discovery is ' + + "single-channel on the metadata type registry's `filePatterns` " + + '(`MetadataTypeSchema`, registered via `registerMetadataTypeSchema` / the default ' + + 'registry), which `contributes.kinds` never extended; if plugin-extensible ' + + 'discovery is ever wanted, it gets designed against that registry, not revived ' + + 'here', + reason: + 'ADR-0049 enforce-or-remove; #11169, maintainer ruling 2026-08-24 (「接受你的建议。」) on ' + + 'the aligned four-facet analysis. The sub-field was declared-but-unenforced on an ' + + 'authorable published surface: the schema promised that declaring `globs` "enables the ' + + 'system to parse and validate new file types" (its own example: a BI plugin handling ' + + '`*.report.ts`), and the platform accepted it, stored it, and served it back through ' + + '`GET /metadata/kind` — while the discovery the description promised never ran, because ' + + 'real glob-driven artifact discovery reads `filePatterns` off the metadata type registry ' + + 'and `metadata-plugin.zod.ts` records outright that `contributes.kinds` does not extend ' + + 'it. Measured in PR #11168 and re-verified at claim time with the card\'s positive ' + + 'control: zero value reads anywhere (the only non-test occurrences of the path are the ' + + 'schema declaration and two type positions), and no in-repo manifest authors the key ' + + 'outside test fixtures. Enforce was weighed and rejected on all four facets: it would ' + + 'build a SECOND discovery channel parallel to `filePatterns` for a spelling with zero ' + + 'pull. Why D3 semantic and not a D2 conversion: a manifest is not a stack collection ' + + 'member (`PLURAL_TO_SINGULAR` has no `packages`/`plugins` entry), so a conversion would ' + + 'be a transform with no seam that ever runs.', + acceptanceCriteria: + 'An authored `contributes.kinds[].globs` is a loud rejection through every ' + + 'spec-validating path — `retiredKey()` types it `never` (tsc error at the authoring ' + + 'site) and the parse raises the prescription itself (`os plugin build` exits non-zero ' + + 'printing it). `contributes.kinds` with `{ id, description? }` still parses and still ' + + 'registers (`engine` → `registry.registerKind`), and the registered bucket stays ' + + 'reachable via `GET /metadata/kind`. ⚠️ Runtime behaviour is deliberately UNCHANGED ' + + 'and must be verified as such: nothing read the value, so removing it removes no ' + + 'behaviour; the `registerKind` / `getAllKinds` type positions drop `globs` from their ' + + 'declared shapes (a type-only change — the parameter widens). A stored kind item that ' + + 'still carries `globs` keeps serving as stored data; clear it by deleting the key from ' + + 'the source manifest and republishing.', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index ed4d99b786..0c35d2e6df 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -5172,7 +5172,30 @@ const step18: MigrationStep = { 'other authorable carrier and leave with the key (RETIRED_DEFS_BY_MAJOR[18]); the live ' + 'per-breakpoint channel on a page component is `responsiveStyles` (ADR-0065), which ' + 'objectui really compiles. The mechanical conversion strips the key from stored pages ' + - '(pure lossless delete — it never had an effect to lose).', + '(pure lossless delete — it never had an effect to lose). ' + + 'Finally, it retires nine of the eleven members of the plugin manifest\'s ' + + '`contributes` block (#10724, ADR-0049 enforce-or-remove; triage graded 2026-08-21, ' + + 'cloud census leg discharged clean 2026-08-24): `events`, `menus`, `themes`, ' + + '`translations`, `actions`, `drivers`, `fieldTypes`, `functions` and `commands`. ' + + '#10627 measured — three repos, controlled — that the whole monorepo contains exactly ' + + 'one non-test read of `manifest.contributes`, and it reads `kinds`; the other nine ' + + 'members parsed, entered the manifest, and changed nothing, while published docs and ' + + 'the schema\'s own JSDoc kept teaching them (`commands` documented Commander.js ' + + 'resolution the CLI dropped for oclif; `fieldTypes` advertised a registration seam ' + + 'that never existed). All nine are retiredKey tombstones mirroring `loading`; ' + + '`kinds` survives (live reader) and `routes` is untouched pending its own fork ' + + '(#10726). D3 semantic, no D2 conversion: a manifest is not a stack collection ' + + 'member, so a conversion would be a transform with no seam that ever runs. ' + + 'On the surviving `kinds` bucket it also retires the `globs` sub-field (#11169, ' + + 'ADR-0049 enforce-or-remove; maintainer ruling 2026-08-24): the schema promised ' + + 'that declaring `globs` enables file-type discovery, but discovery globs ' + + '`filePatterns` off the metadata type registry — which `contributes.kinds` does ' + + 'not extend, as `metadata-plugin.zod.ts` records outright — so an authored ' + + '`globs` was accepted, stored, served back through `GET /metadata/kind`, and ' + + 'never consulted (zero value reads; the only non-test occurrences were the ' + + 'schema declaration and two type positions). The `kind` bucket itself and its ' + + '`id` are untouched; file-type discovery stays single-channel on `filePatterns`. ' + + 'D3 semantic `plugin-manifest-kind-globs-retired`, same no-seam reasoning.', conversionIds: [ 'field-malformed-scale-precision-removed', 'record-chatter-position-vocabulary', @@ -6037,6 +6060,98 @@ const step18: MigrationStep = { + 'drift window carrying `indexes[].where` is rejected with the database-layer prescription ' + 'rather than saved with the key silently dropped.', }, + { + id: 'plugin-manifest-contributes-dead-members-retired', + surface: + 'manifest.contributes.events / manifest.contributes.menus / manifest.contributes.themes / ' + + 'manifest.contributes.translations / manifest.contributes.actions / ' + + 'manifest.contributes.drivers / manifest.contributes.fieldTypes / ' + + 'manifest.contributes.functions / manifest.contributes.commands (nine of the block\'s ' + + 'eleven members; `kinds` and `routes` are NOT part of this retirement)', + replacement: + 'delete the keys — each capability already has its one enforced channel: `events` → ' + + "subscribe imperatively in plugin code (`ctx.hook('kernel:ready', …)` from `init`/`start`); " + + '`menus` → the app `navigation` tree or `manifest.navigationContributions` (ADR-0029 D7); ' + + '`themes` → the stack-level `themes` metadata collection (an unrelated `ThemeSchema` ' + + 'surface); `translations` → the `translation` metadata type, authored with ' + + '`defineTranslationBundle` in `defineStack({ translations })`; `actions` → the stack ' + + '`actions` collection or `engine.registerAction`; `drivers` → register a kernel service ' + + 'named `driver.*` (objectql calls `registerDriver` on it); `fieldTypes` → nothing (no ' + + 'registration seam exists; the vocabulary is the spec `FieldType` enum); `functions` → ' + + '`defineStack({ functions })` → `engine.registerFunction`; `commands` → oclif native ' + + "plugin auto-discovery (an `oclif` section in the plugin's own `package.json`; see " + + '`cli-extension.zod.ts`)', + reason: + 'ADR-0049 enforce-or-remove; #10724 (triage graded 2026-08-21, cloud precondition ' + + 'discharged 2026-08-24). #10627 measured, monorepo-wide and non-test with control probes, ' + + 'that the ENTIRE monorepo contains exactly one read of `manifest.contributes` — ' + + '`packages/objectql/src/engine.ts`, member `kinds` — so all nine members above parsed, ' + + 'entered the manifest, and changed nothing. The census stands on three repos: objectstack ' + + '(re-verified on current main at claim time), objectui (0 property reads; control: 63 ' + + 'files carry the bare word), and cloud (measured clean 2026-08-24 at `5b5925a`: zero ' + + '`manifest.contributes` reads, controls held). Several members were actively misleading: ' + + '`events` was authored in-repo by a plugin that already subscribes imperatively; ' + + '`commands` documented Commander.js resolution the CLI dropped for oclif auto-discovery; ' + + '`fieldTypes` advertised a registration seam that has never existed. ' + + 'Why D3 semantic and not a D2 conversion: the conversion chain walks a normalized STACK ' + + 'and `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a ' + + 'stack collection member and a conversion would be a transform with no seam that ever ' + + 'runs (the `kernel/Manifest:loading` precedent, recorded verbatim in its retired-key ' + + 'entry).', + acceptanceCriteria: + 'No `objectstack.config.ts` manifest and no packaged `manifest.json` authors any of the ' + + 'nine members. The enforced channel is the one place a manifest is parsed with an author ' + + 'present: `os plugin build` runs `ManifestSchema.safeParse` and exits non-zero printing ' + + 'the per-key tombstone prescription; TypeScript authors fail earlier still (each key is ' + + 'typed `never`). `contributes.kinds` keeps parsing and registering ' + + '(`registry.registerKind`), and `contributes.routes` is untouched pending its own fork ' + + '(#10726). ⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: ' + + 'nothing ever read the nine members, so removing them removes no behaviour. A package ' + + 'ALREADY INSTALLED whose stored manifest carries one degrades to a single ' + + '`[metadata_spec_invalid]` log line at registration (the registry\'s `validate()` is a ' + + 'diagnostic, not a gate) rather than a boot failure; clear it by deleting the key from ' + + 'the source manifest and reinstalling.', + }, + { + id: 'plugin-manifest-kind-globs-retired', + surface: 'manifest.contributes.kinds[].globs (the `kind` bucket itself and its `id` are untouched)', + replacement: + 'delete the key — a kind entry is `{ id, description? }`. File-type discovery is ' + + "single-channel on the metadata type registry's `filePatterns` " + + '(`MetadataTypeSchema`, registered via `registerMetadataTypeSchema` / the default ' + + 'registry), which `contributes.kinds` never extended; if plugin-extensible ' + + 'discovery is ever wanted, it gets designed against that registry, not revived ' + + 'here', + reason: + 'ADR-0049 enforce-or-remove; #11169, maintainer ruling 2026-08-24 (「接受你的建议。」) on ' + + 'the aligned four-facet analysis. The sub-field was declared-but-unenforced on an ' + + 'authorable published surface: the schema promised that declaring `globs` "enables the ' + + 'system to parse and validate new file types" (its own example: a BI plugin handling ' + + '`*.report.ts`), and the platform accepted it, stored it, and served it back through ' + + '`GET /metadata/kind` — while the discovery the description promised never ran, because ' + + 'real glob-driven artifact discovery reads `filePatterns` off the metadata type registry ' + + 'and `metadata-plugin.zod.ts` records outright that `contributes.kinds` does not extend ' + + 'it. Measured in PR #11168 and re-verified at claim time with the card\'s positive ' + + 'control: zero value reads anywhere (the only non-test occurrences of the path are the ' + + 'schema declaration and two type positions), and no in-repo manifest authors the key ' + + 'outside test fixtures. Enforce was weighed and rejected on all four facets: it would ' + + 'build a SECOND discovery channel parallel to `filePatterns` for a spelling with zero ' + + 'pull. Why D3 semantic and not a D2 conversion: a manifest is not a stack collection ' + + 'member (`PLURAL_TO_SINGULAR` has no `packages`/`plugins` entry), so a conversion would ' + + 'be a transform with no seam that ever runs.', + acceptanceCriteria: + 'An authored `contributes.kinds[].globs` is a loud rejection through every ' + + 'spec-validating path — `retiredKey()` types it `never` (tsc error at the authoring ' + + 'site) and the parse raises the prescription itself (`os plugin build` exits non-zero ' + + 'printing it). `contributes.kinds` with `{ id, description? }` still parses and still ' + + 'registers (`engine` → `registry.registerKind`), and the registered bucket stays ' + + 'reachable via `GET /metadata/kind`. ⚠️ Runtime behaviour is deliberately UNCHANGED ' + + 'and must be verified as such: nothing read the value, so removing it removes no ' + + 'behaviour; the `registerKind` / `getAllKinds` type positions drop `globs` from their ' + + 'declared shapes (a type-only change — the parameter widens). A stored kind item that ' + + 'still carries `globs` keeps serving as stored data; clear it by deleting the key from ' + + 'the source manifest and republishing.', + }, { id: 'record-chatter-position-vocabulary-converged', surface: @@ -6680,6 +6795,150 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // conversion `metric-filters-removed`, which strips the key from every metric // in `analyticsCubes[].measures`. 'data/Metric:filters', + // #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` + // block; one of NINE members tombstoned together. Census, registration major, + // and the why-no-D2-conversion reasoning are recorded once in the sibling + // entry `kernel/Manifest:contributes.events` (this family) and in + // `kernel/Manifest:loading` (the precedent); the D3 semantic entry is + // `plugin-manifest-contributes-dead-members-retired`. + // + // `actions` declared invocable server actions nothing ever registered. The + // working surfaces are the stack `actions` collection (METADATA_ARRAY_KEYS, + // registered by the engine) and `engine.registerAction`. + 'kernel/Manifest:contributes.actions', + // #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` + // block; one of NINE members tombstoned together. Census, registration major, + // and the why-no-D2-conversion reasoning are recorded once in the sibling + // entry `kernel/Manifest:contributes.events` (this family) and in + // `kernel/Manifest:loading` (the precedent); the D3 semantic entry is + // `plugin-manifest-contributes-dead-members-retired`. + // + // `commands` documented Commander.js runtime resolution as current behaviour; + // the CLI never resolved commands from it. Commands are auto-discovered via + // oclif's native plugin system (`cli-extension.zod.ts` records the migration: + // "The `objectstack.config.ts` plugins array no longer determines CLI + // commands"). + 'kernel/Manifest:contributes.commands', + // #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` + // block; one of NINE members tombstoned together. Census, registration major, + // and the why-no-D2-conversion reasoning are recorded once in the sibling + // entry `kernel/Manifest:contributes.events` (this family) and in + // `kernel/Manifest:loading` (the precedent); the D3 semantic entry is + // `plugin-manifest-contributes-dead-members-retired`. + // + // `drivers` declared storage drivers nothing ever read. A driver is wired by + // registering a kernel SERVICE named `driver.*` (objectql's plugin calls + // `registerDriver` on it); the only in-repo author (driver-memory) was + // registered that way, not by its declaration. + 'kernel/Manifest:contributes.drivers', + // #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` + // block (triage graded 2026-08-21; cloud leg measured clean 2026-08-24). One + // of NINE members tombstoned together: #10627 measured exactly ONE non-test + // read of `manifest.contributes` monorepo-wide (engine.ts, member `kinds`), + // with controls, re-verified across objectstack + objectui + cloud at claim + // time. `events` in particular was decorative twice over: its only in-repo + // author (plugin-hono-server) already subscribed to the same events + // imperatively in plugin code, so the declaration drove nothing even for the + // one package that wrote it. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // tombstone ships on the 17.x line (launch-window convention) and the + // prescription lives at the major boundary where `migrate meta` users look. + // + // Registered here but NOT in `src/conversions/registry.ts`, for the reason + // `kernel/Manifest:loading` gives: a package manifest is not a stack + // collection member (`PLURAL_TO_SINGULAR` has no `packages` / `plugins` + // entry), so a D2 conversion would be a transform with no seam that ever + // runs. The prescription reaches authors through the tombstone at + // `os plugin build` → `ManifestSchema.safeParse` and through the D3 semantic + // entry `plugin-manifest-contributes-dead-members-retired`. + 'kernel/Manifest:contributes.events', + // #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` + // block; one of NINE members tombstoned together. Census, registration major, + // and the why-no-D2-conversion reasoning are recorded once in the sibling + // entry `kernel/Manifest:contributes.events` (this family) and in + // `kernel/Manifest:loading` (the precedent); the D3 semantic entry is + // `plugin-manifest-contributes-dead-members-retired`. + // + // `fieldTypes` advertised an extension point the platform does not have: + // there is no `registerFieldType` seam anywhere — zero hits monorepo-wide. + // The field-type vocabulary is the spec `FieldType` enum. + 'kernel/Manifest:contributes.fieldTypes', + // #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` + // block; one of NINE members tombstoned together. Census, registration major, + // and the why-no-D2-conversion reasoning are recorded once in the sibling + // entry `kernel/Manifest:contributes.events` (this family) and in + // `kernel/Manifest:loading` (the precedent); the D3 semantic entry is + // `plugin-manifest-contributes-dead-members-retired`. + // + // `functions` declared ObjectQL functions nothing ever registered from here. + // The working surface is `defineStack({ functions })` → hook-binder → + // `engine.registerFunction`. + 'kernel/Manifest:contributes.functions', + // #11169 — ADR-0049 enforce-or-remove on `contributes.kinds[].globs` + // (maintainer ruling 2026-08-24, 「接受你的建议。」: remove via the full + // ceremony). The sub-field promised glob-driven file-type discovery the + // platform performs somewhere else: real artifact discovery globs + // `filePatterns` off the metadata type registry, and + // `metadata-plugin.zod.ts` states outright that `contributes.kinds` does not + // extend it. Measured (PR #11168, re-run with positive control at claim + // time): zero consumers — the only non-test occurrences of the path are the + // schema declaration and two type positions (`registerKind`'s parameter, + // `getAllKinds`' return), and nothing reads the value — so an authored + // `globs` was stored, served back through `GET /metadata/kind`, and never + // consulted. The `kind` bucket itself and its `id` are NOT touched: the + // bucket is live (engine → `registerKind`) and reachable via the generic + // `GET /metadata/:type` passthrough. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // tombstone ships on the 17.x line (launch-window convention) and the + // prescription lives at the major boundary where `migrate meta` users look. + // + // Registered here but NOT in `src/conversions/registry.ts`, for the reason + // `kernel/Manifest:loading` gives: a package manifest is not a stack + // collection member, so a D2 conversion would be a transform with no seam + // that ever runs. The prescription reaches authors through the tombstone at + // `os plugin build` → `ManifestSchema.safeParse` and through the D3 semantic + // entry `plugin-manifest-kind-globs-retired`. + 'kernel/Manifest:contributes.kinds.globs', + // #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` + // block; one of NINE members tombstoned together. Census, registration major, + // and the why-no-D2-conversion reasoning are recorded once in the sibling + // entry `kernel/Manifest:contributes.events` (this family) and in + // `kernel/Manifest:loading` (the precedent); the D3 semantic entry is + // `plugin-manifest-contributes-dead-members-retired`. + // + // `menus` had the tightest control in the census: the bare word has only three + // non-test hits monorepo-wide — this declaration plus two alias maps that + // redirect the spelling to `navigation`. The working surface is app + // `navigation` / `manifest.navigationContributions` (ADR-0029 D7), which the + // engine registers. + 'kernel/Manifest:contributes.menus', + // #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` + // block; one of NINE members tombstoned together. Census, registration major, + // and the why-no-D2-conversion reasoning are recorded once in the sibling + // entry `kernel/Manifest:contributes.events` (this family) and in + // `kernel/Manifest:loading` (the precedent); the D3 semantic entry is + // `plugin-manifest-contributes-dead-members-retired`. + // + // `themes` here was a `{ id, label, path }` shape with no reader anywhere. It + // is UNRELATED to the stack-level `themes` collection (a `ThemeSchema` + // surface, itself retired as a carrier by `stack-themes-carrier-retired`): + // stack-level theme hits reach the registry through top-level metadata + // collections, never through `contributes.themes`. + 'kernel/Manifest:contributes.themes', + // #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` + // block; one of NINE members tombstoned together. Census, registration major, + // and the why-no-D2-conversion reasoning are recorded once in the sibling + // entry `kernel/Manifest:contributes.events` (this family) and in + // `kernel/Manifest:loading` (the precedent); the D3 semantic entry is + // `plugin-manifest-contributes-dead-members-retired`. + // + // `translations` promised `{ locale, path }` file registration no loader ever + // performed. The working surface is the `translation` metadata type — stack + // `translations` collection (`defineTranslationBundle`), governed by + // `packages/spec/liveness/translation.json`. + 'kernel/Manifest:contributes.translations', // #8586 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-14, ruled // REMOVE). `additionalTypes` was declared, authorable, and documented on four // docs pages as THE way a plugin registers a custom metadata type — and read