From 5cdfa2e807d1ffe7950e8ff463ee5bb0bb27083d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 13:44:19 +0000 Subject: [PATCH 1/3] feat(spec): retire inert MetadataPluginConfig.additionalTypes (#8586, ADR-0049) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01E5tUwGM3LQoqErTfkvRW7W --- ...etadata-plugin-additional-types-retired.md | 62 ++++++++++++ .../docs/plugins/adding-a-metadata-type.mdx | 31 ++++-- .../references/kernel/metadata-plugin.mdx | 24 ++--- packages/metadata-protocol/src/protocol.ts | 6 +- packages/metadata/src/metadata-manager.ts | 8 +- packages/spec/authorable-surface/kernel.json | 2 +- .../additional-types-retirement.test.ts | 94 +++++++++++++++++++ .../spec/src/kernel/metadata-plugin.test.ts | 9 +- .../spec/src/kernel/metadata-plugin.zod.ts | 50 ++++++++-- .../spec/src/kernel/metadata-type-schemas.ts | 15 ++- ...__MetadataPluginConfig__additionalTypes.ts | 25 +++++ ...etadata-plugin-additional-types-retired.ts | 45 +++++++++ packages/spec/src/migrations/registry.ts | 87 ++++++++++++++++- 13 files changed, 409 insertions(+), 49 deletions(-) create mode 100644 .changeset/metadata-plugin-additional-types-retired.md create mode 100644 packages/spec/src/kernel/additional-types-retirement.test.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/18.kernel__MetadataPluginConfig__additionalTypes.ts create mode 100644 packages/spec/src/migrations/entries/semantic/18.metadata-plugin-additional-types-retired.ts diff --git a/.changeset/metadata-plugin-additional-types-retired.md b/.changeset/metadata-plugin-additional-types-retired.md new file mode 100644 index 0000000000..7324bccf10 --- /dev/null +++ b/.changeset/metadata-plugin-additional-types-retired.md @@ -0,0 +1,62 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec): retire the inert `additionalTypes` key from `MetadataPluginConfig` (#8586, ADR-0049) + +**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). + +`MetadataPluginConfig.additionalTypes` was declared, authorable, and documented +on four docs pages as THE way a plugin registers a custom metadata type — and +read by **nothing**. The only production writer of the manager's type registry +is `setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY)`, called exactly once, and +it replaces the array outright: measured on the real `MetadataManager`, +declared count == live count (27 == 27). An author who followed the published +instructions wrote the key, got no error, and nothing happened — the #4212 +`onInstall` silence trap one level down (maintainer-ruled REMOVE, 2026-08-14). + +**What is refused:** an authored `additionalTypes` on `MetadataPluginConfig` +(inline or via the manifest's `config` embed). The key is a `retiredKey()` +tombstone — the schema is not `.strict()`, so a plain deletion would have +silently stripped it — refused at `tsc` (typed `never`) and at the parse +(`invalid_type` at path `additionalTypes`, message carrying the prescription). + +**What stays accepted:** every `MetadataPluginConfig` without the key, +byte-identically. Runtime behaviour is unchanged: nothing ever read the key, +so removing it removes no behaviour. + +The retirement kit: + +- tombstone at the schema (`packages/spec/src/kernel/metadata-plugin.zod.ts`) +- ADR-0087 registration: retired-key entry + `kernel/MetadataPluginConfig:additionalTypes` + D3 semantic entry + `metadata-plugin-additional-types-retired`, both under protocol 18 (no D2 + conversion — a plugin config is not a stack collection member, the + `kernel/Manifest:loading` precedent) +- pin tests (`additional-types-retirement.test.ts`) +- docs corrected: `content/docs/plugins/adding-a-metadata-type.mdx` (four + sites) now describes how a kind actually enters the live set — as a side + effect of registering an item of that kind; the generated reference page + follows the schema +- the two source comments that asserted the phantom growth path + (`metadata-manager.ts`, `metadata-protocol/src/protocol.ts`) and the + `registerMetadataTypeSchema` doc note corrected + +## FROM → TO + +```ts +// before — parsed green; the entries were merged into nothing +const config: MetadataPluginConfig = { + storage: {}, + additionalTypes: [{ type: 'chart', label: 'Chart', filePatterns: ['**/*.chart.ts'], domain: 'ui' }], +}; + +// after — delete the key; register items of the kind instead, and bind its schema +const config: MetadataPluginConfig = { storage: {} }; +// in the plugin: registerMetadataTypeSchema('chart', ChartSchema) from init(ctx); +// the kind enters the live set when an item of it is registered. +``` + + diff --git a/content/docs/plugins/adding-a-metadata-type.mdx b/content/docs/plugins/adding-a-metadata-type.mdx index b04705d459..6248bf5ad2 100644 --- a/content/docs/plugins/adding-a-metadata-type.mdx +++ b/content/docs/plugins/adding-a-metadata-type.mdx @@ -14,8 +14,9 @@ files; a third file is required only if you want a bespoke editor. ## TL;DR ``` -1. Register the type entry: built-in -> DEFAULT_METADATA_TYPE_REGISTRY; - plugin -> additionalTypes on MetadataPluginConfig (packages/spec) +1. Register the type: built-in -> add an entry to DEFAULT_METADATA_TYPE_REGISTRY + (packages/spec); plugin -> register items of the type (the kind enters the + live set as a side effect -- there is no declared-kind config key) 2. Define a Zod schema for the type (packages/spec/src//) 3. (Optional) Register a custom editor (objectui/.../builtinComponents.tsx) ``` @@ -67,11 +68,20 @@ use, not pinned at process start. `DEFAULT_METADATA_TYPE_REGISTRY` is the core built-in array — edit it (and `BUILTIN_METADATA_TYPE_SCHEMAS`, step 2) only for types that ship with the -platform. A third-party package contributes its own types instead through -the **`additionalTypes`** array on `MetadataPluginConfig`, and registers the -matching Zod schema with `registerMetadataTypeSchema(type, schema)` from its -plugin's `init(ctx)` so `GET /api/v1/meta` emits a real JSON Schema. The -registry entry shape is the same in both cases. +platform. It is also the **total universe of declared types**: there is no +config key through which a package declares a new type. (Through v17 the +schema carried an `additionalTypes` array on `MetadataPluginConfig` that was +documented for exactly that — it had no reader anywhere and was retired by +#8586, ADR-0049; authoring it is now a loud parse error.) + +A third-party package's type instead enters the live set **as a side effect +of registering items of that type**: items your package's manifest carries +are registered through `SchemaRegistry.registerItem` during app/manifest +registration, and runtime code can register items with +`MetadataManager.register`. The first registered item admits the type; a +type with no items is not in the live set. Alongside the items, register the +matching Zod schema with `registerMetadataTypeSchema(type, schema)` from +your plugin's `init(ctx)` so `GET /api/v1/meta` emits a real JSON Schema. ## 2. Define the Zod schema @@ -126,11 +136,12 @@ editor consumes from the registered Zod schema. } ``` - `getMetaTypes()` reads this registry on every request, so a type registered + `getMetaTypes()` reads this registry on every request, so a schema registered during `init` is served from the first call onward. Registering the schema does not by itself put the type in the listing — `getMetaTypes()` enumerates types from the engine registry and the metadata service and then decorates - each with its schema, so declare the type (via `additionalTypes`) as well. + each with its schema, so the type must also have at least one registered + item (that side effect is what admits it; see step 1). The Metadata Admin **SchemaForm** consumes the JSON Schema derived from this Zod schema and produces: @@ -232,7 +243,7 @@ If you omit them, the directory falls back to the registry `label`. ## Checklist -- [ ] Registry entry added (`type`, `label`, `domain`, required `filePatterns`, flags) — for plugins, via `additionalTypes` on `MetadataPluginConfig` +- [ ] Type registered: built-in → registry entry added to `DEFAULT_METADATA_TYPE_REGISTRY` (`type`, `label`, `domain`, required `filePatterns`, flags); plugin → at least one item of the type registered (the side effect that admits the type — there is no declared-kind config key) - [ ] Zod schema authored under `packages/spec/src//.zod.ts` - [ ] Zod schema wired up: built-in → `BUILTIN_METADATA_TYPE_SCHEMAS`; plugin → `registerMetadataTypeSchema()` in the plugin's `init()` - [ ] (Optional) Custom editor registered in `builtinComponents.tsx` diff --git a/content/docs/references/kernel/metadata-plugin.mdx b/content/docs/references/kernel/metadata-plugin.mdx index 829d7e90be..fe2860965f 100644 --- a/content/docs/references/kernel/metadata-plugin.mdx +++ b/content/docs/references/kernel/metadata-plugin.mdx @@ -92,15 +92,15 @@ const result = MetadataBulkResultSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **storage** | `{ datasource?: string; tableName?: string; fallback?: Enum<'filesystem' \| 'memory' \| 'none'>; rootDir?: string; … }` | ✅ | Storage backend configuration | -| **customizationPolicies** | `{ metadataType: string; allowCustomization?: boolean; lockedFields?: string[]; customizableFields?: string[]; … }[]` | optional | Default customization policies per type | -| **mergeStrategy** | `{ defaultStrategy?: Enum<'keep-custom' \| 'accept-incoming' \| 'three-way-merge'>; alwaysAcceptIncoming?: string[]; alwaysKeepCustom?: string[]; autoResolveNonConflicting?: boolean }` | optional | Merge strategy for package upgrades | -| **additionalTypes** | `{ label: string; description?: string; filePatterns: string[]; supportsOverlay?: boolean; … }[]` | optional | Additional custom metadata types | -| **enableEvents** | `boolean` | optional | Emit metadata change events | -| **validateOnWrite** | `boolean` | optional | Validate metadata on write | -| **enableVersioning** | `boolean` | optional | Track metadata version history | -| **cacheMaxItems** | `integer` | optional | Max items in memory cache | -| **bootstrap** | `Enum<'eager' \| 'lazy' \| 'artifact-only'>` | optional | How metadata is primed at plugin start (eager / lazy / artifact-only) | +| **storage** | `{ datasource?: string; tableName: string; fallback: Enum<'filesystem' \| 'memory' \| 'none'>; rootDir?: string; … }` | ✅ | Storage backend configuration | +| **customizationPolicies** | `{ metadataType: string; allowCustomization: boolean; lockedFields?: string[]; customizableFields?: string[]; … }[]` | optional | Default customization policies per type | +| **mergeStrategy** | `{ defaultStrategy: Enum<'keep-custom' \| 'accept-incoming' \| 'three-way-merge'>; alwaysAcceptIncoming?: string[]; alwaysKeepCustom?: string[]; autoResolveNonConflicting: boolean }` | optional | Merge strategy for package upgrades | +| **additionalTypes** | `never` | optional | [REMOVED] `config.additionalTypes` was removed from `MetadataPluginConfig` in @objectstack/spec 17 (#8586, ADR-0049 enforce-or-remove) — it never had an effect: the only production writer of the metadata type registry is `setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY)`, which replaces the array outright, so nothing ever merged these entries and the live type set was exactly the built-in registry whatever you declared here. Delete the key. There is no declared-kind channel: a kind enters the live metadata-type set as a side effect of registering an ITEM of that kind (`SchemaRegistry.registerItem` during app/manifest registration, or `MetadataManager.register` at runtime); bind its schema with `registerMetadataTypeSchema(type, schema)` from your plugin's `init(ctx)` so `GET /api/v1/meta` serves a real JSON Schema for it. | +| **enableEvents** | `boolean` | ✅ | Emit metadata change events | +| **validateOnWrite** | `boolean` | ✅ | Validate metadata on write | +| **enableVersioning** | `boolean` | ✅ | Track metadata version history | +| **cacheMaxItems** | `integer` | ✅ | Max items in memory cache | +| **bootstrap** | `Enum<'eager' \| 'lazy' \| 'artifact-only'>` | ✅ | How metadata is primed at plugin start (eager / lazy / artifact-only) | --- @@ -115,9 +115,9 @@ const result = MetadataBulkResultSchema.parse(data); | **name** | `'ObjectStack Metadata Service'` | ✅ | Plugin name | | **version** | `string` | ✅ | Plugin version | | **type** | `'standard'` | ✅ | Plugin type | -| **description** | `string` | optional | Plugin description | -| **capabilities** | `{ crud?: boolean; query?: boolean; overlay?: boolean; watch?: boolean; … }` | ✅ | Plugin capabilities | -| **config** | `{ storage: object; customizationPolicies?: object[]; mergeStrategy?: object; additionalTypes?: object[]; … }` | optional | Plugin configuration | +| **description** | `string` | ✅ | Plugin description | +| **capabilities** | `{ crud: boolean; query: boolean; overlay: boolean; watch: boolean; … }` | ✅ | Plugin capabilities | +| **config** | `{ storage: object; customizationPolicies?: object[]; mergeStrategy?: object; enableEvents: boolean; … }` | optional | Plugin configuration | --- diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 783f7f7f66..4bf3167247 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -4373,8 +4373,10 @@ export class ObjectStackProtocolImplementation implements * MetadataManager knows. Its `typeRegistry` is seeded with * `DEFAULT_METADATA_TYPE_REGISTRY` in the manager's constructor, so * early in boot this source contributes only declared types; it grows - * later (artifact load, `additionalTypes`) and is read for the types - * the SchemaRegistry has not been told about. + * later as items are registered (artifact load, runtime `register()`) + * and is read for the types the SchemaRegistry has not been told + * about. (This comment used to also name `additionalTypes` as a growth + * path — that key never had a reader and was retired by #8586.) * * Extracted from {@link getMetaTypes} rather than copied: the listing and * {@link reportUnhydratableOrgScopedRows} must answer "which types exist diff --git a/packages/metadata/src/metadata-manager.ts b/packages/metadata/src/metadata-manager.ts index 7927781ef9..53a167d4cb 100644 --- a/packages/metadata/src/metadata-manager.ts +++ b/packages/metadata/src/metadata-manager.ts @@ -2466,9 +2466,11 @@ export class MetadataManager implements IMetadataService { const entry = this.typeRegistry.find(e => e.type === type); if (!entry) return undefined; - // Merge declarative (live registry entry — covers built-ins AND - // plugin-contributed `additionalTypes`) + plugin-registered type-level - // actions. Deduped by name; imperatively-registered actions win on + // Merge declarative (live registry entry — the built-in + // `DEFAULT_METADATA_TYPE_REGISTRY`, the registry's only production writer; + // the plugin-contributed `additionalTypes` channel this comment used to + // claim never existed and was retired by #8586) + plugin-registered + // type-level actions. Deduped by name; imperatively-registered actions win on // collision. Emitted so the metadata-admin engine can render per-type // buttons (e.g. datasource "Test connection"). Omit the key entirely // when the type has none, to keep the response lean. diff --git a/packages/spec/authorable-surface/kernel.json b/packages/spec/authorable-surface/kernel.json index 484eba96d9..688f225863 100644 --- a/packages/spec/authorable-surface/kernel.json +++ b/packages/spec/authorable-surface/kernel.json @@ -394,7 +394,7 @@ "kernel/MetadataOverlay:tenantId", "kernel/MetadataOverlay:updatedAt", "kernel/MetadataOverlay:updatedBy", - "kernel/MetadataPluginConfig:additionalTypes", + "kernel/MetadataPluginConfig:additionalTypes [RETIRED]", "kernel/MetadataPluginConfig:bootstrap", "kernel/MetadataPluginConfig:cacheMaxItems", "kernel/MetadataPluginConfig:customizationPolicies", diff --git a/packages/spec/src/kernel/additional-types-retirement.test.ts b/packages/spec/src/kernel/additional-types-retirement.test.ts new file mode 100644 index 0000000000..8cd6fa6e29 --- /dev/null +++ b/packages/spec/src/kernel/additional-types-retirement.test.ts @@ -0,0 +1,94 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { MetadataPluginConfigSchema, MetadataPluginManifestSchema } from './metadata-plugin.zod'; + +// ─── [#8586] `MetadataPluginConfig.additionalTypes` is REMOVED ──────────────── +// +// ADR-0049 enforce-or-remove, maintainer ruling 2026-08-14, ruled REMOVE. The +// key was declared, authorable, and documented on four docs pages as THE way a +// plugin registers a custom metadata type — and read by NOTHING: the only +// production writer of the manager's type registry is +// `setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY)` (`packages/metadata/src/ +// plugin.ts`), called exactly once, and it replaces the array outright. +// Measured on the real `MetadataManager`: declared count == live count +// (27 == 27). The same silence trap as #4212's `onInstall`, one level down: +// write it per the docs, get no error, nothing happens. +// +// Route: `retiredKey()` tombstone, NOT plain deletion. +// `MetadataPluginConfigSchema` is not `.strict()`, so deleting the key would +// make zod strip it in silence — replacing an inert declaration with an +// invisible one (the #3726 / #3733 shape, ADR-0104). The tombstone is audible +// in two channels: `tsc` (the key's input type is `never`) and the parse below. +// +// ⚠️ On the assertion set (the #4914 precedent, same reasoning): the dispatch +// asked for the unknown-key refusal shape, but that shape belongs to `.strict()` +// schemas — a `retiredKey()` tombstone raises `invalid_type` from its +// `z.never()`, with the prescription as the message (`shared/retired-key.ts`; +// `alias-integrity.test.ts` records the same fact). And the ADR-0112 `code` + +// `status` envelope belongs to the API error surface — a schema refusal raises +// a `ZodError` whose issues carry `code` and `path` but no `status`. So these +// pins assert the strongest set this surface really has: refusal, the issue +// `code`, the `path` naming WHICH key was refused, and the prescription text +// (#5240: where the wording is the contract, pin the wording). +describe('[#8586] MetadataPluginConfig.additionalTypes retirement', () => { + /** A config that is valid except for whatever the individual test adds. */ + const baseConfig = { storage: {} } as const; + + it('REJECTS an authored `additionalTypes`, naming the key and carrying the fix', () => { + const result = MetadataPluginConfigSchema.safeParse({ + ...baseConfig, + additionalTypes: [{ + type: 'chart', + label: 'Chart', + filePatterns: ['**/*.chart.ts'], + domain: 'ui', + }], + }); + + expect(result.success).toBe(false); + if (result.success) return; // narrowing; the assertion above already failed + + const issue = result.error.issues.find((i) => i.path[0] === 'additionalTypes'); + expect(issue, 'the refusal must name `additionalTypes`').toBeDefined(); + // The machine-readable half of the envelope this surface actually has. + expect(issue!.code).toBe('invalid_type'); + expect(issue!.path).toEqual(['additionalTypes']); + // The prescription itself — this string IS the migration doc for whoever + // hits it, so it is contract, not commentary. + expect(issue!.message).toMatch(/`config\.additionalTypes`.*removed.*17.*#8586/s); + expect(issue!.message).toMatch(/Delete the key/s); + // The live mechanism must be named: how a kind ACTUALLY enters the set. + expect(issue!.message).toMatch(/registering an ITEM/s); + expect(issue!.message).toMatch(/registerMetadataTypeSchema/s); + }); + + it('REJECTS it through the manifest embed too (`config.additionalTypes`)', () => { + const result = MetadataPluginManifestSchema.safeParse({ + id: 'com.objectstack.metadata', + name: 'ObjectStack Metadata Service', + version: '1.0.0', + type: 'standard', + capabilities: {}, + config: { ...baseConfig, additionalTypes: [] }, + }); + + expect(result.success).toBe(false); + if (result.success) return; + const issue = result.error.issues.find( + (i) => i.path[0] === 'config' && i.path[1] === 'additionalTypes', + ); + expect(issue, 'the refusal must surface at config.additionalTypes').toBeDefined(); + expect(issue!.code).toBe('invalid_type'); + }); + + it('parses cleanly once the key is deleted, and grows no `additionalTypes` property', () => { + const parsed = MetadataPluginConfigSchema.parse({ ...baseConfig }); + expect(parsed.enableEvents).toBe(true); // control: defaults still apply + // The non-strict strip path: absence must stay absence. If the tombstone + // were ever replaced by a plain deletion, an authored `additionalTypes` + // would be stripped here in silence — this pin plus the rejections above + // are what make that regression loud. + expect(parsed).not.toHaveProperty('additionalTypes'); + }); +}); diff --git a/packages/spec/src/kernel/metadata-plugin.test.ts b/packages/spec/src/kernel/metadata-plugin.test.ts index 11bb625f51..b5bf9e8750 100644 --- a/packages/spec/src/kernel/metadata-plugin.test.ts +++ b/packages/spec/src/kernel/metadata-plugin.test.ts @@ -322,12 +322,8 @@ describe('MetadataPluginProtocol', () => { defaultStrategy: 'three-way-merge' as const, alwaysKeepCustom: ['fields.*.label'], }, - additionalTypes: [{ - type: 'chart', - label: 'Chart', - filePatterns: ['**/*.chart.ts'], - domain: 'ui', - }], + // `additionalTypes` was retired by #8586 (ADR-0049) — authoring it is + // now a parse error; see additional-types-retirement.test.ts for the pins. enableEvents: true, validateOnWrite: true, enableVersioning: true, @@ -337,7 +333,6 @@ describe('MetadataPluginProtocol', () => { const result = MetadataPluginConfigSchema.parse(config); expect(result.storage.datasource).toBe('default'); expect(result.customizationPolicies).toHaveLength(1); - expect(result.additionalTypes).toHaveLength(1); expect(result.cacheMaxItems).toBe(5000); }); diff --git a/packages/spec/src/kernel/metadata-plugin.zod.ts b/packages/spec/src/kernel/metadata-plugin.zod.ts index 575f36b269..5215b3cb3e 100644 --- a/packages/spec/src/kernel/metadata-plugin.zod.ts +++ b/packages/spec/src/kernel/metadata-plugin.zod.ts @@ -1,6 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; +import { retiredKey } from '../shared/retired-key'; import { MetadataManagerConfigSchema } from './metadata-loader.zod'; import { MergeStrategyConfigSchema, CustomizationPolicySchema } from './metadata-customization.zod'; import { ActionSchema } from '../ui/action.zod'; @@ -52,7 +53,12 @@ import { ActionSchema } from '../ui/action.zod'; * * The canonical list of all metadata types managed by the platform. * Each type maps to a specific Zod schema (e.g., ObjectSchema, ViewSchema). - * Plugins can extend this registry via `contributes.kinds` in the manifest. + * There is no declared-kind channel beyond this list (#8586): a plugin's kind + * enters the LIVE type set only as a side effect of registering an item of + * that kind (`SchemaRegistry.registerItem` / `MetadataManager.register`). + * A manifest's `contributes.kinds` stores kind DESCRIPTORS as items of the + * `kind` type (`SchemaRegistry.registerKind`); it does not extend this enum + * or the type registry. * * ## Naming Convention * **IMPORTANT:** All metadata type names are in **SINGULAR** form: @@ -473,12 +479,36 @@ export const MetadataPluginConfigSchema = lazySchema(() => z.object({ .describe('Merge strategy for package upgrades'), /** - * Additional metadata type registrations. - * Used by plugins to register custom metadata types beyond the built-in set. + * REMOVED in v17 (#8586, ADR-0049 enforce-or-remove). + * + * `additionalTypes` was declared, documented on four docs pages as THE way a + * plugin registers a custom metadata type, and read by NOTHING: the only + * production writer of the manager's type registry is + * `setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY)` (`packages/metadata/src/ + * plugin.ts`), called exactly once, and it REPLACES the array outright. + * Measured: declared count == live count (27 == 27) — the live type set is + * exactly the built-in registry, whatever was authored here. The same + * silence trap as #4212's `onInstall`, one level down: write it per the + * docs, get no error, nothing happens. + * + * Tombstoned rather than deleted because `MetadataPluginConfigSchema` is not + * `.strict()`: a plain deletion would silently strip the key, replacing an + * inert declaration with an invisible one (the #3726 / #3733 shape, + * ADR-0104). How a kind ACTUALLY reaches the live set is in the + * prescription below. */ - additionalTypes: z.array(MetadataTypeRegistryEntryBaseSchema.omit({ type: true }).extend({ - type: z.string().describe('Custom metadata type identifier'), - })).optional().describe('Additional custom metadata types'), + additionalTypes: retiredKey( + '`config.additionalTypes` was removed from `MetadataPluginConfig` in @objectstack/spec 17 ' + + '(#8586, ADR-0049 enforce-or-remove) — it never had an effect: the only production writer ' + + 'of the metadata type registry is `setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY)`, which ' + + 'replaces the array outright, so nothing ever merged these entries and the live type set ' + + 'was exactly the built-in registry whatever you declared here. Delete the key. There is no ' + + 'declared-kind channel: a kind enters the live metadata-type set as a side effect of ' + + 'registering an ITEM of that kind (`SchemaRegistry.registerItem` during app/manifest ' + + 'registration, or `MetadataManager.register` at runtime); bind its schema with ' + + "`registerMetadataTypeSchema(type, schema)` from your plugin's `init(ctx)` so " + + '`GET /api/v1/meta` serves a real JSON Schema for it.', + ), /** * Enable metadata change events. @@ -612,7 +642,13 @@ export type MetadataPluginManifestParsed = z.infer ], }; @@ -5206,6 +5255,40 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> 'ui/RecordDetailsProps:layout', // ], + // Post-cut retirements: v17.0.0 was cut before these landed, so the + // tombstone ships on the 17.x line (launch-window convention) and the + // prescription registers at the major boundary where `migrate meta` users + // look — the step18 discipline, applied to retired keys. + 18: [ + // One file per entry under `entries/retired-keys/`, concatenated here sorted by + // entry id by `gen:migration-registry` (#7297). Add an entry by adding a + // FILE — never by editing between the markers, which is generated. + // + // #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 + // by NOTHING: the only production writer of the manager's type registry is + // `setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY)`, called exactly once, and + // it replaces the array outright. Measured: declared count == live count + // (27 == 27). The #4212 `onInstall` silence trap one level down. + // + // 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: accept-set + // narrowings ride minor releases) and the prescription lives at the major + // boundary where `migrate meta` users look (the #8495 / PR #8666 precedent). + // + // Registered here but NOT in `src/conversions/registry.ts`, for the reason + // `kernel/Manifest:loading` gives: the conversion chain walks a normalized + // STACK (`mapCollection(stack, 'objects' | 'views' | …)`) and + // `applyConversionsToStoredItem` maps a metadata type onto one of those + // collections. A metadata-plugin config is neither — there is no `plugins` + // entry in `PLURAL_TO_SINGULAR`, so it is not a stack collection member and a + // MetadataConversion here would be a transform with no seam that ever runs. + // The prescription reaches authors through the tombstone (`tsc` + the parse) + // and the D3 semantic entry `metadata-plugin-additional-types-retired`. + 'kernel/MetadataPluginConfig:additionalTypes', + // + ], }; /** From f03dd9c7711a6fe7a3c27d5c5551720988a9113a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 15:16:58 +0000 Subject: [PATCH 2/3] merge origin/main (os-regen artifacts taken from main; regeneration follows) --- .../references/kernel/metadata-plugin.mdx | 24 +++++++++---------- packages/spec/authorable-surface/kernel.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/content/docs/references/kernel/metadata-plugin.mdx b/content/docs/references/kernel/metadata-plugin.mdx index fe2860965f..829d7e90be 100644 --- a/content/docs/references/kernel/metadata-plugin.mdx +++ b/content/docs/references/kernel/metadata-plugin.mdx @@ -92,15 +92,15 @@ const result = MetadataBulkResultSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **storage** | `{ datasource?: string; tableName: string; fallback: Enum<'filesystem' \| 'memory' \| 'none'>; rootDir?: string; … }` | ✅ | Storage backend configuration | -| **customizationPolicies** | `{ metadataType: string; allowCustomization: boolean; lockedFields?: string[]; customizableFields?: string[]; … }[]` | optional | Default customization policies per type | -| **mergeStrategy** | `{ defaultStrategy: Enum<'keep-custom' \| 'accept-incoming' \| 'three-way-merge'>; alwaysAcceptIncoming?: string[]; alwaysKeepCustom?: string[]; autoResolveNonConflicting: boolean }` | optional | Merge strategy for package upgrades | -| **additionalTypes** | `never` | optional | [REMOVED] `config.additionalTypes` was removed from `MetadataPluginConfig` in @objectstack/spec 17 (#8586, ADR-0049 enforce-or-remove) — it never had an effect: the only production writer of the metadata type registry is `setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY)`, which replaces the array outright, so nothing ever merged these entries and the live type set was exactly the built-in registry whatever you declared here. Delete the key. There is no declared-kind channel: a kind enters the live metadata-type set as a side effect of registering an ITEM of that kind (`SchemaRegistry.registerItem` during app/manifest registration, or `MetadataManager.register` at runtime); bind its schema with `registerMetadataTypeSchema(type, schema)` from your plugin's `init(ctx)` so `GET /api/v1/meta` serves a real JSON Schema for it. | -| **enableEvents** | `boolean` | ✅ | Emit metadata change events | -| **validateOnWrite** | `boolean` | ✅ | Validate metadata on write | -| **enableVersioning** | `boolean` | ✅ | Track metadata version history | -| **cacheMaxItems** | `integer` | ✅ | Max items in memory cache | -| **bootstrap** | `Enum<'eager' \| 'lazy' \| 'artifact-only'>` | ✅ | How metadata is primed at plugin start (eager / lazy / artifact-only) | +| **storage** | `{ datasource?: string; tableName?: string; fallback?: Enum<'filesystem' \| 'memory' \| 'none'>; rootDir?: string; … }` | ✅ | Storage backend configuration | +| **customizationPolicies** | `{ metadataType: string; allowCustomization?: boolean; lockedFields?: string[]; customizableFields?: string[]; … }[]` | optional | Default customization policies per type | +| **mergeStrategy** | `{ defaultStrategy?: Enum<'keep-custom' \| 'accept-incoming' \| 'three-way-merge'>; alwaysAcceptIncoming?: string[]; alwaysKeepCustom?: string[]; autoResolveNonConflicting?: boolean }` | optional | Merge strategy for package upgrades | +| **additionalTypes** | `{ label: string; description?: string; filePatterns: string[]; supportsOverlay?: boolean; … }[]` | optional | Additional custom metadata types | +| **enableEvents** | `boolean` | optional | Emit metadata change events | +| **validateOnWrite** | `boolean` | optional | Validate metadata on write | +| **enableVersioning** | `boolean` | optional | Track metadata version history | +| **cacheMaxItems** | `integer` | optional | Max items in memory cache | +| **bootstrap** | `Enum<'eager' \| 'lazy' \| 'artifact-only'>` | optional | How metadata is primed at plugin start (eager / lazy / artifact-only) | --- @@ -115,9 +115,9 @@ const result = MetadataBulkResultSchema.parse(data); | **name** | `'ObjectStack Metadata Service'` | ✅ | Plugin name | | **version** | `string` | ✅ | Plugin version | | **type** | `'standard'` | ✅ | Plugin type | -| **description** | `string` | ✅ | Plugin description | -| **capabilities** | `{ crud: boolean; query: boolean; overlay: boolean; watch: boolean; … }` | ✅ | Plugin capabilities | -| **config** | `{ storage: object; customizationPolicies?: object[]; mergeStrategy?: object; enableEvents: boolean; … }` | optional | Plugin configuration | +| **description** | `string` | optional | Plugin description | +| **capabilities** | `{ crud?: boolean; query?: boolean; overlay?: boolean; watch?: boolean; … }` | ✅ | Plugin capabilities | +| **config** | `{ storage: object; customizationPolicies?: object[]; mergeStrategy?: object; additionalTypes?: object[]; … }` | optional | Plugin configuration | --- diff --git a/packages/spec/authorable-surface/kernel.json b/packages/spec/authorable-surface/kernel.json index 688f225863..484eba96d9 100644 --- a/packages/spec/authorable-surface/kernel.json +++ b/packages/spec/authorable-surface/kernel.json @@ -394,7 +394,7 @@ "kernel/MetadataOverlay:tenantId", "kernel/MetadataOverlay:updatedAt", "kernel/MetadataOverlay:updatedBy", - "kernel/MetadataPluginConfig:additionalTypes [RETIRED]", + "kernel/MetadataPluginConfig:additionalTypes", "kernel/MetadataPluginConfig:bootstrap", "kernel/MetadataPluginConfig:cacheMaxItems", "kernel/MetadataPluginConfig:customizationPolicies", From f154dd36c6d75da58cb31f85bb2648e802975f72 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 15:21:49 +0000 Subject: [PATCH 3/3] chore(spec): regenerate registry and reference artifacts after merging main (Batch D analytics entry absorbed; 86 semantic) --- .../references/kernel/metadata-plugin.mdx | 24 +++++++++---------- packages/spec/authorable-surface/kernel.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/content/docs/references/kernel/metadata-plugin.mdx b/content/docs/references/kernel/metadata-plugin.mdx index 829d7e90be..fe2860965f 100644 --- a/content/docs/references/kernel/metadata-plugin.mdx +++ b/content/docs/references/kernel/metadata-plugin.mdx @@ -92,15 +92,15 @@ const result = MetadataBulkResultSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **storage** | `{ datasource?: string; tableName?: string; fallback?: Enum<'filesystem' \| 'memory' \| 'none'>; rootDir?: string; … }` | ✅ | Storage backend configuration | -| **customizationPolicies** | `{ metadataType: string; allowCustomization?: boolean; lockedFields?: string[]; customizableFields?: string[]; … }[]` | optional | Default customization policies per type | -| **mergeStrategy** | `{ defaultStrategy?: Enum<'keep-custom' \| 'accept-incoming' \| 'three-way-merge'>; alwaysAcceptIncoming?: string[]; alwaysKeepCustom?: string[]; autoResolveNonConflicting?: boolean }` | optional | Merge strategy for package upgrades | -| **additionalTypes** | `{ label: string; description?: string; filePatterns: string[]; supportsOverlay?: boolean; … }[]` | optional | Additional custom metadata types | -| **enableEvents** | `boolean` | optional | Emit metadata change events | -| **validateOnWrite** | `boolean` | optional | Validate metadata on write | -| **enableVersioning** | `boolean` | optional | Track metadata version history | -| **cacheMaxItems** | `integer` | optional | Max items in memory cache | -| **bootstrap** | `Enum<'eager' \| 'lazy' \| 'artifact-only'>` | optional | How metadata is primed at plugin start (eager / lazy / artifact-only) | +| **storage** | `{ datasource?: string; tableName: string; fallback: Enum<'filesystem' \| 'memory' \| 'none'>; rootDir?: string; … }` | ✅ | Storage backend configuration | +| **customizationPolicies** | `{ metadataType: string; allowCustomization: boolean; lockedFields?: string[]; customizableFields?: string[]; … }[]` | optional | Default customization policies per type | +| **mergeStrategy** | `{ defaultStrategy: Enum<'keep-custom' \| 'accept-incoming' \| 'three-way-merge'>; alwaysAcceptIncoming?: string[]; alwaysKeepCustom?: string[]; autoResolveNonConflicting: boolean }` | optional | Merge strategy for package upgrades | +| **additionalTypes** | `never` | optional | [REMOVED] `config.additionalTypes` was removed from `MetadataPluginConfig` in @objectstack/spec 17 (#8586, ADR-0049 enforce-or-remove) — it never had an effect: the only production writer of the metadata type registry is `setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY)`, which replaces the array outright, so nothing ever merged these entries and the live type set was exactly the built-in registry whatever you declared here. Delete the key. There is no declared-kind channel: a kind enters the live metadata-type set as a side effect of registering an ITEM of that kind (`SchemaRegistry.registerItem` during app/manifest registration, or `MetadataManager.register` at runtime); bind its schema with `registerMetadataTypeSchema(type, schema)` from your plugin's `init(ctx)` so `GET /api/v1/meta` serves a real JSON Schema for it. | +| **enableEvents** | `boolean` | ✅ | Emit metadata change events | +| **validateOnWrite** | `boolean` | ✅ | Validate metadata on write | +| **enableVersioning** | `boolean` | ✅ | Track metadata version history | +| **cacheMaxItems** | `integer` | ✅ | Max items in memory cache | +| **bootstrap** | `Enum<'eager' \| 'lazy' \| 'artifact-only'>` | ✅ | How metadata is primed at plugin start (eager / lazy / artifact-only) | --- @@ -115,9 +115,9 @@ const result = MetadataBulkResultSchema.parse(data); | **name** | `'ObjectStack Metadata Service'` | ✅ | Plugin name | | **version** | `string` | ✅ | Plugin version | | **type** | `'standard'` | ✅ | Plugin type | -| **description** | `string` | optional | Plugin description | -| **capabilities** | `{ crud?: boolean; query?: boolean; overlay?: boolean; watch?: boolean; … }` | ✅ | Plugin capabilities | -| **config** | `{ storage: object; customizationPolicies?: object[]; mergeStrategy?: object; additionalTypes?: object[]; … }` | optional | Plugin configuration | +| **description** | `string` | ✅ | Plugin description | +| **capabilities** | `{ crud: boolean; query: boolean; overlay: boolean; watch: boolean; … }` | ✅ | Plugin capabilities | +| **config** | `{ storage: object; customizationPolicies?: object[]; mergeStrategy?: object; enableEvents: boolean; … }` | optional | Plugin configuration | --- diff --git a/packages/spec/authorable-surface/kernel.json b/packages/spec/authorable-surface/kernel.json index 484eba96d9..688f225863 100644 --- a/packages/spec/authorable-surface/kernel.json +++ b/packages/spec/authorable-surface/kernel.json @@ -394,7 +394,7 @@ "kernel/MetadataOverlay:tenantId", "kernel/MetadataOverlay:updatedAt", "kernel/MetadataOverlay:updatedBy", - "kernel/MetadataPluginConfig:additionalTypes", + "kernel/MetadataPluginConfig:additionalTypes [RETIRED]", "kernel/MetadataPluginConfig:bootstrap", "kernel/MetadataPluginConfig:cacheMaxItems", "kernel/MetadataPluginConfig:customizationPolicies",