diff --git a/.changeset/manifest-contributes-routes-retired.md b/.changeset/manifest-contributes-routes-retired.md new file mode 100644 index 0000000000..582a31f905 --- /dev/null +++ b/.changeset/manifest-contributes-routes-retired.md @@ -0,0 +1,49 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec): retire `contributes.routes` — the plugin-manifest block's last dead member (#10726, ADR-0049 enforce-or-remove; maintainer-ruled Option B 2026-08-22) + + + +**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). + +`contributes.routes` was the one member #10724's nine-member retirement +deliberately excluded: removing it needed a ruling, not a tombstone, because +the key was the only *declared* channel for a real capability (serving a +code-handler endpoint) and four published surfaces — a customer-published +skill among them — taught it as working machinery. The measurement (#10627, +controlled, three repos, cloud leg closed clean by #10812) is that nothing +ever read it: the HttpDispatcher never registered a prefix from the +declaration, so an entry parsed cleanly and served nothing. The maintainer +ruled Option B (remove; redirect the author-facing materials to the +imperative mount). The doc corrections landed first (PR #11327); this change +is the removal half, plus the two remaining teaching sites (#11328): the +worked manifest example in `plugin-rest-api.zod.ts` and the `router` +delivered-form comments in `metadata-plugin.zod.ts`. + +**What is refused:** authoring `contributes.routes`. It is a `retiredKey()` +tombstone (neither `ManifestSchema` nor the `contributes` object is +`.strict()`, so a plain deletion would have silently stripped the key), so +authoring it is a `tsc` error and a parse error carrying the prescription. + +**FROM → TO:** + +- `contributes.routes: [{ prefix, service, methods? }]` → mount the route + imperatively: resolve the `http.server` service from the plugin context and + register the handler on `kernel:ready`; delete the key. A declarative + endpoint over a pipeline the platform already runs (query/return records, + trigger a flow) is `defineStack({ apis })`. + +**What stays:** `contributes.kinds`, now the block's sole live member +(engine → `registry.registerKind`). Runtime behaviour is unchanged: nothing +ever read the key, so removing it 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-routes-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/content/docs/references/api/dispatcher.mdx b/content/docs/references/api/dispatcher.mdx index fba66f32f4..15cf69a75f 100644 --- a/content/docs/references/api/dispatcher.mdx +++ b/content/docs/references/api/dispatcher.mdx @@ -16,9 +16,10 @@ The dispatcher is the central routing component that: 3. Returns 503 Service Unavailable when a service is not registered 4. Serves prefixes registered by the kernel services above. Plugins that need a code handler mount it imperatively on the `http.server` service (resolve - it from the plugin context on `kernel:ready`), NOT through the manifest's - `contributes.routes` key — nothing reads that key, so an entry there parses - cleanly and serves nothing. + it from the plugin context on `kernel:ready`) — the manifest's + `contributes.routes` key was removed in @objectstack/spec 17 (#10726): + nothing ever read it, and authoring it is now a tsc error and a parse + error carrying that prescription. Architecture alignment: - Kubernetes: API server aggregation layer diff --git a/content/docs/references/api/metadata.mdx b/content/docs/references/api/metadata.mdx index 4e211f0db1..bf00da501d 100644 --- a/content/docs/references/api/metadata.mdx +++ b/content/docs/references/api/metadata.mdx @@ -88,7 +88,7 @@ const result = AppDefinitionResponseSchema.parse(data); | **homePageId** | `never` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (#4667, #4709, ADR-0049). objectui's console did read it before v17 (`resolveLandingRoute`), so this key had a consumer — it was retired because the capability is better expressed on the navigation item itself than as an ID cross-reference that silently falls back when it dangles. An app's landing page IS its first navigation item (by `order`), and the root landing follows `isDefault` routing. Delete the key; to change where an app opens, reorder `navigation` so the intended entry is first, and set `isDefault` on the app that should own the root landing. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **requiredPermissions** | `string[]` | optional | Permissions required to access this app | | **objects** | `never` | optional | [REMOVED] `App.objects` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read; the spec itself labelled it "config file convenience"). Objects belong to the stack (`defineStack({ objects })`); an app reaches them through its navigation items. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **apis** | `never` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key and declare the endpoint one level up, on the STACK: `defineStack({ apis })`. That surface EXECUTES from protocol 17 (#5040). Between #4936 and the executor landing it was refused wholesale — nothing mounted a declared path, so every key including `authRequired` parsed and gated nothing — and that blanket refusal is now narrowed to five per-endpoint publish gates (namespace, supported target, mapping, policy, uniqueness): an endpoint that passes them is mounted and serves traffic as soon as the stack is published. Two things to get right when you move it: the path must sit inside your own carve-out, `/api/v1/apps//` with an explicit `manifest.namespace` (ADR-0121 D1/D2), and `authRequired` defaults to `true` — an explicit `false` is the only thing that opens anonymous access, and ADR-0121 D6 then requires an armed `rateLimit: { enabled: true, windowMs, maxRequests }`. Read the `declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a security review, not a rename. A route that genuinely needs handler CODE is mounted imperatively instead: resolve the `http.server` service from your plugin context and register the route on `kernel:ready` (NOT the manifest `contributes.routes` key — nothing reads it, so an entry there parses cleanly and serves nothing). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **apis** | `never` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key and declare the endpoint one level up, on the STACK: `defineStack({ apis })`. That surface EXECUTES from protocol 17 (#5040). Between #4936 and the executor landing it was refused wholesale — nothing mounted a declared path, so every key including `authRequired` parsed and gated nothing — and that blanket refusal is now narrowed to five per-endpoint publish gates (namespace, supported target, mapping, policy, uniqueness): an endpoint that passes them is mounted and serves traffic as soon as the stack is published. Two things to get right when you move it: the path must sit inside your own carve-out, `/api/v1/apps//` with an explicit `manifest.namespace` (ADR-0121 D1/D2), and `authRequired` defaults to `true` — an explicit `false` is the only thing that opens anonymous access, and ADR-0121 D6 then requires an armed `rateLimit: { enabled: true, windowMs, maxRequests }`. Read the `declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a security review, not a rename. A route that genuinely needs handler CODE is mounted imperatively instead: resolve the `http.server` service from your plugin context and register the route on `kernel:ready` (NOT the manifest `contributes.routes` key — removed in @objectstack/spec 17, #10726: nothing ever read it, and authoring it is now rejected with its own prescription). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **sharing** | `never` | optional | [REMOVED] `App.sharing` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049 enforce-or-remove) — no public-app route ever read it, so it declared sharing that did not exist. Public access is granted per FORM VIEW (`FormView.sharing`, the public-data-collection surface). Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **embed** | `never` | optional | [REMOVED] `App.embed` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049) — no iframe route ever read it. Embedding is a per-form-view surface (`FormView.sharing`), not an app-level switch. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **mobileNavigation** | `never` | optional | [REMOVED] `App.mobileNavigation` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — fully unimplemented; no renderer, including packages/mobile, ever read it). Delete the key; the block returns if/when a real mobile navigation ships. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | diff --git a/content/docs/references/api/package-api.mdx b/content/docs/references/api/package-api.mdx index 3e83d72e60..b1ea120a07 100644 --- a/content/docs/references/api/package-api.mdx +++ b/content/docs/references/api/package-api.mdx @@ -200,7 +200,7 @@ Install package request | **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[]; routes?: object[] }` | optional | Platform contributions | +| **contributes** | `{ kinds?: 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 | @@ -359,7 +359,7 @@ Upgrade package request | **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[]; routes?: object[] }` | optional | Platform contributions | +| **contributes** | `{ kinds?: 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 | @@ -443,7 +443,7 @@ Resolve dependencies request | **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[]; routes?: object[] }` | optional | Platform contributions | +| **contributes** | `{ kinds?: 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/content/docs/references/api/plugin-rest-api.mdx b/content/docs/references/api/plugin-rest-api.mdx index 46ffb56abd..231b503bac 100644 --- a/content/docs/references/api/plugin-rest-api.mdx +++ b/content/docs/references/api/plugin-rest-api.mdx @@ -28,41 +28,31 @@ Architecture Alignment: - Microsoft Dynamics: Web API with entity operations - Strapi: Auto-generated REST endpoints from schemas -@example Plugin Manifest +@example Serving routes from a plugin (imperative `http.server` mount) ```typescript -{ - "name": "rest_api", - "version": "1.0.0", - "type": "server", - "contributes": { - "routes": [ - { - "prefix": "/api/v1/discovery", - "service": "metadata", - "methods": ["getDiscovery"], - "middleware": [ - { "name": "response_envelope", "type": "transformation", "enabled": true } - ] - }, - { - "prefix": "/api/v1/meta", - "service": "metadata", - "methods": ["getMetaTypes", "getMetaItems", "getMetaItem", "saveMetaItem"], - "middleware": [ - { "name": "auth", "type": "authentication", "enabled": true }, - { "name": "request_validation", "type": "validation", "enabled": true } - ] - }, - { - "prefix": "/api/v1/data", - "service": "data", - "methods": ["findData", "getData", "createData", "updateData", "deleteData"] - } - ] +// Routes are mounted in CODE — resolve the `http.server` service from the +// plugin context and register handlers on `kernel:ready` (the service is +// registered by plugin-hono-server; `examples/app-showcase`'s +// recalc-endpoint is a real consumer of this exact shape). The worked +// manifest example that used to sit here declared `contributes.routes`, +// which was removed in @objectstack/spec 17 (#10726): nothing ever read +// it, so every route it showed parsed cleanly and served nothing. +class RestApiPlugin { + name = 'rest_api'; + async init(ctx: PluginContext) { + ctx.hook('kernel:ready', async () => { + const server = await ctx.getService('http.server'); + server.get('/api/v1/discovery', (req, res) => { ... }); + server.post('/api/v1/data/:object', (req, res) => { ... }); + }); } } ``` +A declarative endpoint over a pipeline the platform already runs +(query/return records, trigger a flow) is `defineStack({ apis })` instead — +no plugin code at all. + **Source:** `packages/spec/src/api/plugin-rest-api.zod.ts` diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 49fa4f840a..ed35372299 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -1731,7 +1731,7 @@ Install package request | **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[]; routes?: object[] }` | optional | Platform contributions | +| **contributes** | `{ kinds?: 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/content/docs/references/kernel/manifest.mdx b/content/docs/references/kernel/manifest.mdx index cbea312efd..5d89fc0fa7 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[]; routes?: object[] }` | optional | Platform contributions | +| **contributes** | `{ kinds?: 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 | @@ -83,7 +83,7 @@ Structured plugin permission grants (ADR-0025 §3.2) | **drivers** | `never` | optional | [REMOVED] `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. | | **fieldTypes** | `never` | optional | [REMOVED] `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. | | **functions** | `never` | optional | [REMOVED] `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`. | -| **routes** | `{ prefix: string; service: string; methods?: string[] }[]` | optional | API route contributions to HttpDispatcher | +| **routes** | `never` | optional | [REMOVED] `manifest.contributes.routes` was removed in @objectstack/spec 17 (#10726, ADR-0049 enforce-or-remove) — nothing ever read it: the HttpDispatcher never registered a prefix from the declaration, so an entry here parsed cleanly and served nothing while published material kept recommending it. Delete the key. A route that needs real handler CODE is mounted imperatively: resolve the `http.server` service from the plugin context and register the handler on `kernel:ready`. A declarative endpoint over a pipeline the platform already runs (query/return records, trigger a flow) is `defineStack({ apis })`. | | **commands** | `never` | optional | [REMOVED] `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. | ### Nested Shape: `Manifest.data[number]` diff --git a/content/docs/references/kernel/package-registry.mdx b/content/docs/references/kernel/package-registry.mdx index 7bc51a41af..96ae353ad6 100644 --- a/content/docs/references/kernel/package-registry.mdx +++ b/content/docs/references/kernel/package-registry.mdx @@ -204,7 +204,7 @@ Install package request | **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[]; routes?: object[] }` | optional | Platform contributions | +| **contributes** | `{ kinds?: 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 | @@ -299,7 +299,7 @@ Installed package with runtime lifecycle state | **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[]; routes?: object[] }` | optional | Platform contributions | +| **contributes** | `{ kinds?: 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/content/docs/references/kernel/package-upgrade.mdx b/content/docs/references/kernel/package-upgrade.mdx index 65d6fc6b24..ea3fb2afa3 100644 --- a/content/docs/references/kernel/package-upgrade.mdx +++ b/content/docs/references/kernel/package-upgrade.mdx @@ -153,7 +153,7 @@ Upgrade package request | **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[]; routes?: object[] }` | optional | Platform contributions | +| **contributes** | `{ kinds?: 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 | @@ -295,7 +295,7 @@ Pre-upgrade state snapshot for rollback capability | **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[]; routes?: object[] }` | optional | Platform contributions | +| **contributes** | `{ kinds?: 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/content/docs/references/ui/app.mdx b/content/docs/references/ui/app.mdx index 224ebce57a..89ae404f7e 100644 --- a/content/docs/references/ui/app.mdx +++ b/content/docs/references/ui/app.mdx @@ -87,7 +87,7 @@ const result = ActionNavItemSchema.parse(data); | **homePageId** | `never` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (#4667, #4709, ADR-0049). objectui's console did read it before v17 (`resolveLandingRoute`), so this key had a consumer — it was retired because the capability is better expressed on the navigation item itself than as an ID cross-reference that silently falls back when it dangles. An app's landing page IS its first navigation item (by `order`), and the root landing follows `isDefault` routing. Delete the key; to change where an app opens, reorder `navigation` so the intended entry is first, and set `isDefault` on the app that should own the root landing. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **requiredPermissions** | `string[]` | optional | Permissions required to access this app | | **objects** | `never` | optional | [REMOVED] `App.objects` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read; the spec itself labelled it "config file convenience"). Objects belong to the stack (`defineStack({ objects })`); an app reaches them through its navigation items. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | -| **apis** | `never` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key and declare the endpoint one level up, on the STACK: `defineStack({ apis })`. That surface EXECUTES from protocol 17 (#5040). Between #4936 and the executor landing it was refused wholesale — nothing mounted a declared path, so every key including `authRequired` parsed and gated nothing — and that blanket refusal is now narrowed to five per-endpoint publish gates (namespace, supported target, mapping, policy, uniqueness): an endpoint that passes them is mounted and serves traffic as soon as the stack is published. Two things to get right when you move it: the path must sit inside your own carve-out, `/api/v1/apps//` with an explicit `manifest.namespace` (ADR-0121 D1/D2), and `authRequired` defaults to `true` — an explicit `false` is the only thing that opens anonymous access, and ADR-0121 D6 then requires an armed `rateLimit: { enabled: true, windowMs, maxRequests }`. Read the `declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a security review, not a rename. A route that genuinely needs handler CODE is mounted imperatively instead: resolve the `http.server` service from your plugin context and register the route on `kernel:ready` (NOT the manifest `contributes.routes` key — nothing reads it, so an entry there parses cleanly and serves nothing). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | +| **apis** | `never` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key and declare the endpoint one level up, on the STACK: `defineStack({ apis })`. That surface EXECUTES from protocol 17 (#5040). Between #4936 and the executor landing it was refused wholesale — nothing mounted a declared path, so every key including `authRequired` parsed and gated nothing — and that blanket refusal is now narrowed to five per-endpoint publish gates (namespace, supported target, mapping, policy, uniqueness): an endpoint that passes them is mounted and serves traffic as soon as the stack is published. Two things to get right when you move it: the path must sit inside your own carve-out, `/api/v1/apps//` with an explicit `manifest.namespace` (ADR-0121 D1/D2), and `authRequired` defaults to `true` — an explicit `false` is the only thing that opens anonymous access, and ADR-0121 D6 then requires an armed `rateLimit: { enabled: true, windowMs, maxRequests }`. Read the `declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a security review, not a rename. A route that genuinely needs handler CODE is mounted imperatively instead: resolve the `http.server` service from your plugin context and register the route on `kernel:ready` (NOT the manifest `contributes.routes` key — removed in @objectstack/spec 17, #10726: nothing ever read it, and authoring it is now rejected with its own prescription). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **sharing** | `never` | optional | [REMOVED] `App.sharing` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049 enforce-or-remove) — no public-app route ever read it, so it declared sharing that did not exist. Public access is granted per FORM VIEW (`FormView.sharing`, the public-data-collection surface). Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **embed** | `never` | optional | [REMOVED] `App.embed` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049) — no iframe route ever read it. Embedding is a per-form-view surface (`FormView.sharing`), not an app-level switch. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | | **mobileNavigation** | `never` | optional | [REMOVED] `App.mobileNavigation` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — fully unimplemented; no renderer, including packages/mobile, ever read it). Delete the key; the block returns if/when a real mobile navigation ships. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | 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 2bc687f003..a601efbbf2 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/` | 277 | +| `kernel/` | 276 | | `qa/` | 6 | | `shared/` | 20 | | `system/` | 361 | diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 30eb190982..a30f5886f0 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 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 | +| 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 then executed too (ruled B 2026-08-22; D3 `plugin-manifest-contributes-routes-retired`), tombstoning `routes` and leaving `kinds` the block's sole live member. 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 5b9fbc7bbe..14871b6901 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-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).", + "_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 EXECUTED 2026-08-26 (maintainer-ruled Option B 2026-08-22, cloud precondition discharged by #10812): `routes` is a retiredKey tombstone too (D3 `plugin-manifest-contributes-routes-retired`), leaving `kinds` the block's sole live member. `loading`'s tombstone landed earlier (#4914).", "props": { "id": { "status": "live", @@ -154,9 +154,9 @@ }, "routes": { "status": "dead", - "verifiedAt": "2026-08-23", + "verifiedAt": "2026-08-26", "evidenceScope": "cross-repo", - "note": "#10627 verdict — zero readers, same as its siblings — but a DIFFERENT disposition, which is why it is the one member split onto its own card. Four published surfaces present it as working machinery, one of them a customer-published skill: skills/objectstack-api/SKILL.md tells authors to choose it when \"the endpoint needs real handler CODE\"; packages/spec/src/api/dispatcher.zod.ts:16 says the dispatcher \"supports dynamic route registration from plugins via contributes.routes\"; docs/adr/0088-metadata-kind-admission-and-retirement.md credits it as a delivered form of the `router` kind; content/docs/references/ui/app.mdx sends `App.apis` migrators here. So an author following the shipped skill gets a clean parse and serves nothing. Disposition PENDING: #10726 is OPEN with recommendation B (remove, correcting the four author-facing claims), and B routes the actual removal through #10724 — which is itself OPEN and `pm:blocked` on the cloud census. Nothing has merged; this row is the honest interim state, not a retirement." + "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 (#10726, ADR-0049; maintainer-ruled Option B 2026-08-22, cloud precondition discharged by #10812), ADR-0087 D3 entry `plugin-manifest-contributes-routes-retired`, retired-key entry `kernel/Manifest:contributes.routes`. Prior verdict kept as provenance: #10627 verdict — zero readers, same as its siblings — but a DIFFERENT disposition, which is why it was the one member split onto its own card: four published surfaces presented it as working machinery, one a customer-published skill, so an author following the shipped skill got a clean parse and served nothing. Per the ruling's own sequencing the author-facing corrections landed FIRST (PR #11327: the skill's decision table, dispatcher.zod.ts protocol doc, ADR-0088:40, app.mdx), and the two remaining teaching sites (#11328: the plugin-rest-api.zod.ts worked manifest example, metadata-plugin.zod.ts `router` delivered-form comments) were redirected in the removal PR itself. The capability survives in its working forms: the imperative `http.server` mount (plugin-hono-server registers the service; examples/app-showcase recalc-endpoint mounts on kernel:ready) and declarative `defineStack({ apis })` for pipeline projections." }, "commands": { "status": "dead", diff --git a/packages/spec/src/api/dispatcher.zod.ts b/packages/spec/src/api/dispatcher.zod.ts index 013c749b14..758f7855f6 100644 --- a/packages/spec/src/api/dispatcher.zod.ts +++ b/packages/spec/src/api/dispatcher.zod.ts @@ -15,9 +15,10 @@ import { CoreServiceName, ServiceCriticalitySchema } from '../system/core-servic * 3. Returns 503 Service Unavailable when a service is not registered * 4. Serves prefixes registered by the kernel services above. Plugins that need * a code handler mount it imperatively on the `http.server` service (resolve - * it from the plugin context on `kernel:ready`), NOT through the manifest's - * `contributes.routes` key — nothing reads that key, so an entry there parses - * cleanly and serves nothing. + * it from the plugin context on `kernel:ready`) — the manifest's + * `contributes.routes` key was removed in @objectstack/spec 17 (#10726): + * nothing ever read it, and authoring it is now a tsc error and a parse + * error carrying that prescription. * * Architecture alignment: * - Kubernetes: API server aggregation layer diff --git a/packages/spec/src/api/plugin-rest-api.zod.ts b/packages/spec/src/api/plugin-rest-api.zod.ts index 5f2efff1b2..2cebeab917 100644 --- a/packages/spec/src/api/plugin-rest-api.zod.ts +++ b/packages/spec/src/api/plugin-rest-api.zod.ts @@ -28,40 +28,30 @@ import { MiddlewareConfigSchema } from '../system/http-server.zod'; * - Microsoft Dynamics: Web API with entity operations * - Strapi: Auto-generated REST endpoints from schemas * - * @example Plugin Manifest + * @example Serving routes from a plugin (imperative `http.server` mount) * ```typescript - * { - * "name": "rest_api", - * "version": "1.0.0", - * "type": "server", - * "contributes": { - * "routes": [ - * { - * "prefix": "/api/v1/discovery", - * "service": "metadata", - * "methods": ["getDiscovery"], - * "middleware": [ - * { "name": "response_envelope", "type": "transformation", "enabled": true } - * ] - * }, - * { - * "prefix": "/api/v1/meta", - * "service": "metadata", - * "methods": ["getMetaTypes", "getMetaItems", "getMetaItem", "saveMetaItem"], - * "middleware": [ - * { "name": "auth", "type": "authentication", "enabled": true }, - * { "name": "request_validation", "type": "validation", "enabled": true } - * ] - * }, - * { - * "prefix": "/api/v1/data", - * "service": "data", - * "methods": ["findData", "getData", "createData", "updateData", "deleteData"] - * } - * ] + * // Routes are mounted in CODE — resolve the `http.server` service from the + * // plugin context and register handlers on `kernel:ready` (the service is + * // registered by plugin-hono-server; `examples/app-showcase`'s + * // recalc-endpoint is a real consumer of this exact shape). The worked + * // manifest example that used to sit here declared `contributes.routes`, + * // which was removed in @objectstack/spec 17 (#10726): nothing ever read + * // it, so every route it showed parsed cleanly and served nothing. + * class RestApiPlugin { + * name = 'rest_api'; + * async init(ctx: PluginContext) { + * ctx.hook('kernel:ready', async () => { + * const server = await ctx.getService('http.server'); + * server.get('/api/v1/discovery', (req, res) => { ... }); + * server.post('/api/v1/data/:object', (req, res) => { ... }); + * }); * } * } * ``` + * + * A declarative endpoint over a pipeline the platform already runs + * (query/return records, trigger a flow) is `defineStack({ apis })` instead — + * no plugin code at all. */ // ========================================== diff --git a/packages/spec/src/kernel/manifest.test.ts b/packages/spec/src/kernel/manifest.test.ts index b35f341577..22f55b777d 100644 --- a/packages/spec/src/kernel/manifest.test.ts +++ b/packages/spec/src/kernel/manifest.test.ts @@ -453,16 +453,14 @@ describe('contributes dead-member retirement (#10724, ADR-0049 — tombstoned, n ); }); - it('still parses the two survivors — `kinds` registers, `routes` is untouched (#10726 fork)', () => { + it('still parses the surviving `kinds` member — `routes` retired separately (#10726)', () => { 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', () => { @@ -472,6 +470,43 @@ describe('contributes dead-member retirement (#10724, ADR-0049 — tombstoned, n }); }); +describe('contributes.routes retirement (#10726, ADR-0049 — maintainer-ruled Option B 2026-08-22)', () => { + // The one `contributes` member split onto its own card: zero readers like + // its nine #10724 siblings (#10627's controlled census, cloud leg closed + // clean by #10812), but four published surfaces taught it as THE way to + // serve a code-handler endpoint, so removal needed its own ruling. Neither + // `ManifestSchema` nor the `contributes` object is `.strict()`, so the + // removal is a `retiredKey()` tombstone; the pin asserts the SPECIFIC zod + // issue — located at the key, carrying the removal record and the + // imperative `http.server` fix — never just "it threw". + const base = { id: 'com.example.routes', version: '1.0.0', type: 'plugin', name: 'Routes' }; + + it('REJECTS an authored `contributes.routes` with the prescription as the issue', () => { + const result = ManifestSchema.safeParse({ + ...base, + contributes: { routes: [{ prefix: '/api/v1/example', service: 'example' }] }, + }); + expect(result.success).toBe(false); + if (result.success) return; + const issue = result.error.issues.find( + (i) => i.path[0] === 'contributes' && i.path[1] === 'routes', + ); + expect(issue).toBeDefined(); + expect(issue!.message).toMatch( + /manifest\.contributes\.routes.*removed in @objectstack\/spec 17.*#10726.*Delete the key.*http\.server/s, + ); + }); + + it('still parses the surviving `kinds` member with `routes` simply absent', () => { + const parsed = ManifestSchema.parse({ + ...base, + contributes: { kinds: [{ id: 'sys.bi.report' }] }, + }); + expect(parsed.contributes!.kinds).toHaveLength(1); + expect(parsed.contributes).not.toHaveProperty('routes'); + }); +}); + 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` diff --git a/packages/spec/src/kernel/manifest.zod.ts b/packages/spec/src/kernel/manifest.zod.ts index 2513cd14b9..ddb98ef47c 100644 --- a/packages/spec/src/kernel/manifest.zod.ts +++ b/packages/spec/src/kernel/manifest.zod.ts @@ -317,20 +317,23 @@ export const ManifestSchema = z.object({ /** * Contribution Points (VS Code Style). * - * 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. + * TEN MEMBERS REMOVED in v17.x (ADR-0049 enforce-or-remove): nine at + * #10724 — `events`, `menus`, `themes`, `translations`, `actions`, + * `drivers`, `fieldTypes`, `functions`, `commands` — and `routes` at + * #10726, its own enforce-or-remove fork, maintainer-ruled Option B + * 2026-08-22 (remove; author-facing materials redirect to the imperative + * `http.server` mount) once the cloud census (#10812) closed clean. The + * census behind them (#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). + * Survivor: `kinds` (live reader: engine → `registry.registerKind`) — + * the block's sole remaining live member. */ contributes: z.object({ /** @@ -435,25 +438,17 @@ export const ManifestSchema = z.object({ '`engine.registerFunction`.', ), - /** - * Register API Route Namespaces. - * Declares the API endpoints this plugin provides to the HttpDispatcher. - * The kernel routes matching prefixes to this plugin's handler. - * - * @example - * routes: [ - * { prefix: '/api/v1/i18n', service: 'i18n', methods: ['getLocales', 'getTranslations'] } - * ] - */ - routes: z.array(z.object({ - /** URL path prefix (e.g. "/api/v1/ai") */ - prefix: z.string().regex(/^\//).describe('API path prefix'), - /** Service name this plugin provides */ - service: z.string().describe('Service name this plugin provides'), - /** Protocol method names implemented */ - methods: z.array(z.string()).optional() - .describe('Protocol method names implemented (e.g. ["getLocales", "getTranslations"])'), - })).optional().describe('API route contributions to HttpDispatcher'), + /** REMOVED (#10726) — mount code-handler routes on the `http.server` service; declarative endpoints are `defineStack({ apis })`. */ + routes: retiredKey( + '`manifest.contributes.routes` was removed in @objectstack/spec 17 (#10726, ' + + 'ADR-0049 enforce-or-remove) — nothing ever read it: the HttpDispatcher never ' + + 'registered a prefix from the declaration, so an entry here parsed cleanly and ' + + 'served nothing while published material kept recommending it. Delete the key. ' + + 'A route that needs real handler CODE is mounted imperatively: resolve the ' + + '`http.server` service from the plugin context and register the handler on ' + + '`kernel:ready`. A declarative endpoint over a pipeline the platform already ' + + 'runs (query/return records, trigger a flow) is `defineStack({ apis })`.', + ), /** * REMOVED (#10724) — CLI commands are oclif-auto-discovered, never resolved diff --git a/packages/spec/src/kernel/metadata-plugin.zod.ts b/packages/spec/src/kernel/metadata-plugin.zod.ts index f9e81f7228..1e01be2d3f 100644 --- a/packages/spec/src/kernel/metadata-plugin.zod.ts +++ b/packages/spec/src/kernel/metadata-plugin.zod.ts @@ -116,15 +116,17 @@ 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: declarative `apis:` (router — `contributes.routes` - // parses but serves nothing, #10726), `defineStack({ functions })` - // (function — `contributes.functions` was retired unread, #10724), and the + // code contributions: imperative `http.server` mounts + declarative `apis:` + // (router — the manifest `contributes.routes` spelling never delivered and + // was retired, #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 - // as a KIND because its delivered forms (`contributes.routes`, imperative - // `http.server` mounts) are code contributions. A single `ApiEndpoint` — a + // as a KIND because its delivered form (the imperative `http.server` mount; + // the `contributes.routes` spelling never delivered and was retired, #10726) + // is a code contribution. A single `ApiEndpoint` — a // stable URL plus a policy layer over an existing pipeline — is a declarative // artifact, and it passes all three clauses of ADR-0088's admission test: // 1. INDEPENDENT LIFECYCLE — the endpoint matcher indexes, invalidates and diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.routes.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.routes.ts new file mode 100644 index 0000000000..c09906508f --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__contributes.routes.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #10726 — ADR-0049 enforce-or-remove fork on `contributes.routes`, the ONE +// `contributes` member deliberately excluded from #10724's nine-member +// retirement because removing it needed a ruling, not a tombstone: the key +// was the only DECLARED channel for a real capability (serving a code-handler +// endpoint), and four published surfaces — a customer-published skill among +// them — taught it as working machinery. Maintainer ruled Option B 2026-08-22 +// (「接受所有」 on the decision batch carrying the four-axis analysis): remove +// the key; author-facing materials redirect to the imperative `http.server` +// mount, the form that actually works. The ruling's cloud precondition was +// discharged 2026-08-24 (#10812: cloud @ 5b5925a, zero `manifest.contributes` +// reads, controls green), completing #10627's three-repo census at exactly +// one live read (engine.ts, member `kinds` — now the block's sole survivor). +// +// 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-routes-retired`. +export const entry = 'kernel/Manifest:contributes.routes'; diff --git a/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-contributes-routes-retired.ts b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-contributes-routes-retired.ts new file mode 100644 index 0000000000..7bb1007797 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-contributes-routes-retired.ts @@ -0,0 +1,54 @@ +// 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-routes-retired', + surface: + 'manifest.contributes.routes (the one member #10724 deliberately excluded; ' + + '`kinds` is now the block\'s sole surviving live member)', + replacement: + 'delete the key. A route that needs real handler CODE is mounted imperatively: ' + + 'resolve the `http.server` service from the plugin context and register the ' + + 'handler on `kernel:ready` (plugin-hono-server registers the service; ' + + '`examples/app-showcase` mounts POST /api/v1/showcase/recalc that way). A ' + + 'declarative endpoint over a pipeline the platform already runs — query/return ' + + 'records, trigger a flow — is `defineStack({ apis })` (live since protocol 17, ' + + '#5040)', + reason: + 'ADR-0049 enforce-or-remove; #10726, maintainer ruling 2026-08-22 (Option B of the ' + + 'enforce/remove/enforce-later fork, accepted verbatim 「接受所有」 on the decision ' + + 'batch carrying the four-axis analysis). #10627 measured zero readers of the key ' + + 'monorepo-wide with control probes: the HttpDispatcher never registered a prefix ' + + 'from the declaration, so an entry parsed cleanly and served nothing — while FOUR ' + + 'published surfaces presented it as working machinery, one of them a ' + + 'customer-published skill (`skills/objectstack-api` told authors to choose it when ' + + '"the endpoint needs real handler CODE"). That is ADR-0049\'s silent no-op with a ' + + 'published recommendation attached. Per the ruling\'s own sequencing the ' + + 'author-facing corrections landed FIRST (PR #11327: the skill\'s decision table, ' + + 'the dispatcher protocol doc, ADR-0088:40, app.mdx), and the two remaining ' + + 'teaching sites (#11328: the plugin-rest-api.zod.ts worked manifest example, the ' + + 'metadata-plugin.zod.ts `router` delivered-form comments) are redirected in the ' + + 'removal PR itself. The cloud precondition was discharged 2026-08-24 (#10812: ' + + 'cloud @ 5b5925a, zero `manifest.contributes` reads, controls green). Enforce ' + + '(fork A) was weighed and rejected on all four facets: net-new execution surface ' + + 'plus a prefix-claim authority question (who may claim `/api/v1/…`) for a ' + + 'declarative spelling with zero measured authors, while the capability is already ' + + 'reachable imperatively. 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.routes` 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` — the block\'s sole surviving member — keeps parsing and ' + + 'registering (engine → `registry.registerKind`). No author-facing material still ' + + 'recommends the key: every former teaching site points at the imperative ' + + '`http.server` mount (and `defineStack({ apis })` for declarative projections). ' + + '⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: ' + + 'nothing ever read the member, so removing it 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/registry.ts b/packages/spec/src/migrations/registry.ts index 83c908b276..89ffbb95b0 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -6382,6 +6382,56 @@ const step18: MigrationStep = { + 'diagnostic, not a gate) rather than a boot failure; clear it by deleting the key from ' + 'the source manifest and reinstalling.', }, + { + id: 'plugin-manifest-contributes-routes-retired', + surface: + 'manifest.contributes.routes (the one member #10724 deliberately excluded; ' + + '`kinds` is now the block\'s sole surviving live member)', + replacement: + 'delete the key. A route that needs real handler CODE is mounted imperatively: ' + + 'resolve the `http.server` service from the plugin context and register the ' + + 'handler on `kernel:ready` (plugin-hono-server registers the service; ' + + '`examples/app-showcase` mounts POST /api/v1/showcase/recalc that way). A ' + + 'declarative endpoint over a pipeline the platform already runs — query/return ' + + 'records, trigger a flow — is `defineStack({ apis })` (live since protocol 17, ' + + '#5040)', + reason: + 'ADR-0049 enforce-or-remove; #10726, maintainer ruling 2026-08-22 (Option B of the ' + + 'enforce/remove/enforce-later fork, accepted verbatim 「接受所有」 on the decision ' + + 'batch carrying the four-axis analysis). #10627 measured zero readers of the key ' + + 'monorepo-wide with control probes: the HttpDispatcher never registered a prefix ' + + 'from the declaration, so an entry parsed cleanly and served nothing — while FOUR ' + + 'published surfaces presented it as working machinery, one of them a ' + + 'customer-published skill (`skills/objectstack-api` told authors to choose it when ' + + '"the endpoint needs real handler CODE"). That is ADR-0049\'s silent no-op with a ' + + 'published recommendation attached. Per the ruling\'s own sequencing the ' + + 'author-facing corrections landed FIRST (PR #11327: the skill\'s decision table, ' + + 'the dispatcher protocol doc, ADR-0088:40, app.mdx), and the two remaining ' + + 'teaching sites (#11328: the plugin-rest-api.zod.ts worked manifest example, the ' + + 'metadata-plugin.zod.ts `router` delivered-form comments) are redirected in the ' + + 'removal PR itself. The cloud precondition was discharged 2026-08-24 (#10812: ' + + 'cloud @ 5b5925a, zero `manifest.contributes` reads, controls green). Enforce ' + + '(fork A) was weighed and rejected on all four facets: net-new execution surface ' + + 'plus a prefix-claim authority question (who may claim `/api/v1/…`) for a ' + + 'declarative spelling with zero measured authors, while the capability is already ' + + 'reachable imperatively. 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.routes` 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` — the block\'s sole surviving member — keeps parsing and ' + + 'registering (engine → `registry.registerKind`). No author-facing material still ' + + 'recommends the key: every former teaching site points at the imperative ' + + '`http.server` mount (and `defineStack({ apis })` for declarative projections). ' + + '⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: ' + + 'nothing ever read the member, so removing it 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)', @@ -7216,6 +7266,31 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // `navigation` / `manifest.navigationContributions` (ADR-0029 D7), which the // engine registers. 'kernel/Manifest:contributes.menus', + // #10726 — ADR-0049 enforce-or-remove fork on `contributes.routes`, the ONE + // `contributes` member deliberately excluded from #10724's nine-member + // retirement because removing it needed a ruling, not a tombstone: the key + // was the only DECLARED channel for a real capability (serving a code-handler + // endpoint), and four published surfaces — a customer-published skill among + // them — taught it as working machinery. Maintainer ruled Option B 2026-08-22 + // (「接受所有」 on the decision batch carrying the four-axis analysis): remove + // the key; author-facing materials redirect to the imperative `http.server` + // mount, the form that actually works. The ruling's cloud precondition was + // discharged 2026-08-24 (#10812: cloud @ 5b5925a, zero `manifest.contributes` + // reads, controls green), completing #10627's three-repo census at exactly + // one live read (engine.ts, member `kinds` — now the block's sole survivor). + // + // 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-routes-retired`. + 'kernel/Manifest:contributes.routes', // #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 diff --git a/packages/spec/src/ui/app.zod.ts b/packages/spec/src/ui/app.zod.ts index 90a31eab6a..f0bdeac987 100644 --- a/packages/spec/src/ui/app.zod.ts +++ b/packages/spec/src/ui/app.zod.ts @@ -1461,7 +1461,8 @@ export const AppSchema = lazySchema(() => strictObject( 'security review, not a rename. A route that genuinely needs handler CODE is mounted ' + 'imperatively instead: resolve the `http.server` service from your plugin context and ' + 'register the route on `kernel:ready` (NOT the manifest `contributes.routes` key — ' + - 'nothing reads it, so an entry there parses cleanly and serves nothing). ' + + 'removed in @objectstack/spec 17, #10726: nothing ever read it, and authoring it is ' + + 'now rejected with its own prescription). ' + 'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.', ),