diff --git a/.changeset/20233-ui-plugin-migration-guidance-tracker-free.md b/.changeset/20233-ui-plugin-migration-guidance-tracker-free.md new file mode 100644 index 00000000000..433130a1b92 --- /dev/null +++ b/.changeset/20233-ui-plugin-migration-guidance-tracker-free.md @@ -0,0 +1,21 @@ +--- +'@objectstack/spec': patch +--- + +fix(spec): `os migrate meta` guidance for the `ui-*` and `plugin-*` migration entries states each lesson in words instead of citing tracker numbers + +Clause-②: no + +The ADR-0087 semantic entries of the `ui-*` family (component props rows, form-field +and list-view refusals, the react-tier `ListView` aliases, and the retired +interaction, notification, embed, widget and i18n vocabularies) and of the `plugin-*` +family (the plugin manifest, runtime, health-monitor and security-scanner retirements) +are printed by `os migrate meta` as the header, `why:` and `verify:` lines of a manual +change. Their text sent the reader to issue-tracker numbers — some of which no longer +resolve — for what a ruling, measurement or fix had decided; it now says what was +decided, in the sentence being read. The same holds for the two `surface` headers that +carried a number. ADR ids are kept. + +Text only: no entry id, `from` / `to`, conversion or matching logic changes, and the +chain rewrites exactly what it rewrote before. The generated migration registry, +`spec-changes.json` and the protocol upgrade guide carry the same text. diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index 0999dba8280..6527af74278 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -393,13 +393,13 @@ This is a RUNTIME registration API, not stored metadata, so — like `hook-conte - Why not automatic: An uninstall that named no organization matched EVERY organization's rows — measured at 5 of 5 deleted, including a foreign org's (#7705, #7780). That width was never chosen; it fell out of a missing argument, and the two transports of the same route disagreed because of it. In protocol 17 the call is REFUSED instead: neither `organizationId` nor `allTenants: true` answers 400 `TENANT_SCOPE_REQUIRED` and deletes nothing, as does supplying both (they are contradictory, not redundant). Whether a given caller meant "this tenant" or "every tenant" is an intent no transform can recover: `resolveActiveOrganizationId` is catch-wrapped, so an accidental org-less call and a deliberate environment-wide one are byte-identical at the call site — which is the whole reason the parameter had to become explicit rather than conventional. Nothing in authored metadata spells this: it is a runtime call-site contract, so it is one semantic TODO for operators and API callers rather than a stack conversion — the same disposition `rest-requireauth-default-flip` (#12) takes for its own default flip. - Done when: Every caller of `deletePackage` states its tenant scope. A caller that intends an environment-wide uninstall passes `allTenants: true`; a caller that intends a scoped one passes `organizationId`; no caller passes both. An explicit `allTenants: false` is treated as undeclared and refused, since it is not an affirmative request for cross-tenant semantics. Verify the refusal is not merely absorbed: a 400 `TENANT_SCOPE_REQUIRED` reaching a deploy script that previously "succeeded" means that script was relying on the cross-tenant reading and must now say so on purpose. The org-scoped path is unchanged — an uninstall carrying an `organizationId` still removes that org's rows AND the environment-wide (`organization_id IS NULL`) rows, exactly as #7705 left it. - **`plugin-activation-events-retired`** — `kernel.dynamicLoadRequest.activationEvents / studio.studioPluginManifest.activationEvents` → (removed — delete the key. Every plugin activates immediately on load/registration, which is the only behaviour that has ever existed; `activate()` still runs at registration time. Lazy activation, if built, returns via the enforce route of ADR-0049 through a new ADR, with a vocabulary its executor actually honours) - - Why not automatic: Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary they embedded (`onCommand` / `onRoute` / … / `onView` after the #4653 convergence) — promised lazy plugin activation ("plugins remain dormant until an activation event fires") that no runtime in objectstack, cloud, cloud-v1 or objectui ever implemented: nothing anywhere read the key, every plugin activates immediately, and cloud-v1's own ROADMAP recorded lazy activation as unimplemented (planned v0.4.0). That is the ADR-0049 false-compliance shape in the semantically-lying direction: an author writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` expected deferral and got eager activation with a clean parse. Neither parent shape is stored metadata — `StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin` (a root schema, never part of a stack tree) and `DynamicLoadRequest` is a runtime request shape with no caller — so no `sys_metadata` row can carry the key and there is no source for the D2 chain to rewrite; this entry is the D3 record. The kernel key is tombstoned via `retiredKey()` (its schema is not `.strict()`; a plain delete would strip an authored value silently), the studio key is rejected by the strict manifest parse with a guidance prescription (as are its former VS Code-flavoured aliases `activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / `ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys (#3950: an exported schema with no consumer is read as a capability). #4657. SUPERSEDED ON THE KERNEL SIDE by #4834 (same unreleased major): the whole `DynamicLoadRequest` shape — and the rest of the plugin-runtime family with it — was removed, which took this key's `retiredKey()` tombstone with it. That is strictly stronger than the tombstone, not weaker: there is no longer a `DynamicLoadRequest` to author the key INTO, so the prescription an author needs is no longer "delete this key" but "this request shape does not exist" (see `plugin-runtime-family-retired`). The studio half of this entry is unaffected and still enforced by the strict manifest parse. - - Done when: No `defineStudioPlugin` input authors `activationEvents` — authoring it is an unknown key on the strict studio manifest and a parse error carrying the prescription. On the kernel side the stronger #4834 criterion applies instead: there is no `DynamicLoadRequest` type or schema left to author it into at all. No code imports `ActivationEventSchema` / `ActivationEvent` from `@objectstack/spec/kernel` or `@objectstack/spec/studio` (TS2305 after upgrade). Runtime behaviour is byte-identical: plugins loaded eagerly before and after. + - Why not automatic: Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary they embedded (`onCommand` / `onRoute` / … / `onView`, once the kernel and studio copies had converged on the kernel's structured `{ type, pattern }` shape) — promised lazy plugin activation ("plugins remain dormant until an activation event fires") that no runtime in objectstack, cloud, cloud-v1 or objectui ever implemented: nothing anywhere read the key, every plugin activates immediately, and cloud-v1's own ROADMAP recorded lazy activation as unimplemented (planned v0.4.0). That is the ADR-0049 false-compliance shape in the semantically-lying direction: an author writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` expected deferral and got eager activation with a clean parse. Neither parent shape is stored metadata — `StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin` (a root schema, never part of a stack tree) and `DynamicLoadRequest` is a runtime request shape with no caller — so no `sys_metadata` row can carry the key and there is no source for the D2 chain to rewrite; this entry is the D3 record. The kernel key is tombstoned via `retiredKey()` (its schema is not `.strict()`; a plain delete would strip an authored value silently), the studio key is rejected by the strict manifest parse with a guidance prescription (as are its former VS Code-flavoured aliases `activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / `ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys (the lesson of the unwired plugin sandboxing / integrity / approval config removed before this: an exported schema with no consumer is read as a capability). Both keys took ADR-0049's REMOVE answer, not ENFORCE, while protocol 17 was still unreleased. SUPERSEDED ON THE KERNEL SIDE by the maintainer's REMOVE ruling on the rest of the plugin-runtime family (same unreleased major): the whole `DynamicLoadRequest` shape — and the rest of the plugin-runtime family with it — was removed, which took this key's `retiredKey()` tombstone with it. That is strictly stronger than the tombstone, not weaker: there is no longer a `DynamicLoadRequest` to author the key INTO, so the prescription an author needs is no longer "delete this key" but "this request shape does not exist" (see `plugin-runtime-family-retired`). The studio half of this entry is unaffected and still enforced by the strict manifest parse. + - Done when: No `defineStudioPlugin` input authors `activationEvents` — authoring it is an unknown key on the strict studio manifest and a parse error carrying the prescription. On the kernel side the stronger criterion of the plugin-runtime family's removal applies instead: there is no `DynamicLoadRequest` type or schema left to author it into at all. No code imports `ActivationEventSchema` / `ActivationEvent` from `@objectstack/spec/kernel` or `@objectstack/spec/studio` (TS2305 after upgrade). Runtime behaviour is byte-identical: plugins loaded eagerly before and after. - **`plugin-manifest-loading-retired`** — `manifest.loading (the whole block: strategy / preload / codeSplitting / dynamicImport / initialization / dependencyResolution / hotReload / caching / sandboxing / monitoring)` → nothing to re-declare — delete the key. Plugins are composed at boot: `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder` in `packages/core/src/plugin-order.ts`). For the isolation `loading.sandboxing` appeared to configure, note that the plugin trust tier (`manifest.runtime`, ADR-0025 §3.6) does not supply it either: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the manifest permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation - - Why not automatic: ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a complete plugin loading policy and NOTHING read it. A bare-name scan of all three repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), each with a control probe proving the scan saw the tree — put every hit inside `packages/spec` itself: this module's own declaration, its own unit tests, the `Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key parsed, entered the manifest, and changed nothing — #3950, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform isolates plugins, wrote the config, and received a clean parse and zero isolation. An inert security control is worse than an absent one because it is believed. Hot reload was additionally a TWO-SOURCE defect: the docs pointed at this dead `PluginHotReloadSchema` while the only implementation body, `HotReloadManager` (`packages/core/src/hot-reload.ts`), reads a different vocabulary — `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`. Ruling §2 converges on the surviving side: that schema is KEPT as the starting point for a future enforce decision (it has an implementation body but no runtime composes it yet), and enforcing it is deliberately a separate decision, not this retirement. Why D3 semantic and not a D2 conversion: the chain walks a normalized STACK and `applyConversionsToStoredItem` maps a metadata type onto one of its collections. A package manifest is neither — `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a stack collection member and a stored manifest row passes that seam through unchanged. A conversion would be a transform with no seam that ever runs. + - Why not automatic: ADR-0049 enforce-or-remove; the maintainer ruled REMOVE on 2026-08-04, on condition that a bare-name sweep of cloud and objectui came back clean first. The block declared a complete plugin loading policy and NOTHING read it. A bare-name scan of all three repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), each with a control probe proving the scan saw the tree — put every hit inside `packages/spec` itself: this module's own declaration, its own unit tests, the `Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key parsed, entered the manifest, and changed nothing — an exported schema with no consumer, read as a capability, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform isolates plugins, wrote the config, and received a clean parse and zero isolation. An inert security control is worse than an absent one because it is believed. Hot reload was additionally a TWO-SOURCE defect: the docs pointed at this dead `PluginHotReloadSchema` while the only implementation body, `HotReloadManager` (`packages/core/src/hot-reload.ts`), reads a different vocabulary — `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`. Ruling §2 converges on the surviving side: that schema is KEPT as the starting point for a future enforce decision (it has an implementation body but no runtime composes it yet), and enforcing it is deliberately a separate decision, not this retirement. Why D3 semantic and not a D2 conversion: the chain walks a normalized STACK and `applyConversionsToStoredItem` maps a metadata type onto one of its collections. A package manifest is neither — `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a stack collection member and a stored manifest row passes that seam through unchanged. A conversion would be a transform with no seam that ever runs. - Done when: No `objectstack.plugin.json` and no stored package manifest carries a `loading` key. The enforced channel is the one place a manifest is parsed with an author present: `os plugin build` runs `ManifestSchema.safeParse` and exits non-zero, printing the tombstone prescription, so a manifest still declaring `loading` fails its build rather than shipping. TypeScript authors get it earlier still — `loading` is typed `never`, so assigning it is a `tsc` error. ⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: nothing ever read the block, so removing it removes no behaviour. A package ALREADY INSTALLED whose stored manifest carries `loading` keeps working — the registry's `validate()` is an explicit diagnostic and not a gate (it catches, logs `[metadata_spec_invalid]`, and registers the item anyway, deliberately, so bad metadata is never a data outage), so such a row degrades to one log line at registration rather than a boot failure. Clear it by deleting the key from the source manifest and reinstalling. - **`plugin-runtime-family-retired`** — `kernel.dynamicLoadRequest / kernel.dynamicUnloadRequest / kernel.dynamicPluginResult / kernel.pluginSource / kernel.dynamicPluginOperation` → (removed — there is no replacement shape, because there is no operation to describe. Plugins are composed at boot: `defineStack` registers them and the kernel runs register → init → start; the set is fixed until the process restarts. Delete the import and the value. Runtime plugin loading, if it is ever built, returns via the enforce route of ADR-0049 through a new ADR — loader first, vocabulary second) - - Why not automatic: The five schemas declared the "Dynamic Loading" capability — runtime load / unload / reload of plugins without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy — and NOTHING implemented it. A bare-name scan of objectstack, cloud and objectui found zero references outside this package's own declaration, its unit tests and the generated artifacts: no runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the published IDE bundle as proof the platform hot-loads plugins and constructs a request that parses clean and is received by nobody (#3950: an exported schema with no consumer is read as a capability). The #3896 follow-up removed this module's discovery/sandbox config island and left these five in place explicitly — "operation contracts, not security promises; the enforce-or-remove call on them is a design decision rather than a correction" — but that suspension lived only in a changeset paragraph with no issue carrying it. #4834 is that decision, answered REMOVE. `experimental` was considered and rejected: it is only `.describe()` prose and cannot stop an import, the weakest of the three ADR-0049 channels. None of the five is stored metadata — they are root request/result payload shapes embedded in no parent schema and parsed against no metadata document — so no `sys_metadata` row can carry one and there is no source for the D2 chain to rewrite; this entry is the D3 record. The removal also subsumes the kernel half of `plugin-activation-events-retired` (#4657): that tombstone goes with the shape that carried it. ADR-0049, #4834. + - Why not automatic: The five schemas declared the "Dynamic Loading" capability — runtime load / unload / reload of plugins without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy — and NOTHING implemented it. A bare-name scan of objectstack, cloud and objectui found zero references outside this package's own declaration, its unit tests and the generated artifacts: no runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the published IDE bundle as proof the platform hot-loads plugins and constructs a request that parses clean and is received by nobody (an exported schema with no consumer is read as a capability). The earlier removal of this module's discovery/sandbox config island — plugin sandboxing, integrity and approval settings that nothing read — left these five in place explicitly: "operation contracts, not security promises; the enforce-or-remove call on them is a design decision rather than a correction" — but that suspension lived only in a changeset paragraph with no issue carrying it. The maintainer's ruling of 2026-08-03 is that decision, answered REMOVE: hot loading is a real future capability, but nothing is being built and nothing pulls it, and when it is built its vocabulary enters the schema with the implementation. `experimental` was considered and rejected: it is only `.describe()` prose and cannot stop an import, the weakest of the three ADR-0049 channels. None of the five is stored metadata — they are root request/result payload shapes embedded in no parent schema and parsed against no metadata document — so no `sys_metadata` row can carry one and there is no source for the D2 chain to rewrite; this entry is the D3 record. The removal also subsumes the kernel half of `plugin-activation-events-retired`: that tombstone goes with the shape that carried it. ADR-0049. - Done when: No code imports `DynamicLoadRequestSchema`, `DynamicUnloadRequestSchema`, `DynamicPluginResultSchema`, `PluginSourceSchema`, `DynamicPluginOperationSchema` or any of their type aliases (`DynamicLoadRequest`, `DynamicUnloadRequest`, `DynamicPluginResult`, `PluginSource`, `DynamicPluginOperation`, `DynamicLoadRequestInput`, `DynamicUnloadRequestInput`) from `@objectstack/spec` or `@objectstack/spec/kernel` — every one is TS2305 after upgrade, on every public entry (pinned by symbol identity in `plugin-runtime-retirement.test.ts`). Nothing regresses at runtime, because nothing called anything: a caller that believed it was hot-loading a plugin was already only building an object. Boot-time composition through `defineStack` is unchanged. - **`position-permissions-column-retired`** — `sys_position.permissions — the "JSON-serialized array of permission strings" textarea column left the platform position table declared by plugin-security (packages/plugins/plugin-security/src/objects/sys-position.object.ts), together with the clone_position copy entry that carried it between rows` → nothing on this table — delete the key from any authored `sys_position` seed row (stack `data` entries) or data-door write that still carries it. There are no direct position-level permission strings anywhere on the platform: capability reaches a position ONLY through permission-set bindings (`sys_position_permission_set` rows, created in Setup or by an app's kernel:ready binder) and is resolved from the position `name` at request time. A value that was recording intent as documentation belongs in `description`, which remains declared - Why not automatic: Maintainer ruling 2026-08-20 (#9885), ADR-0049 enforce-or-remove: REMOVE. The object-scoped census (all sys_position-naming files, with same-object positive controls resolving `active` / `delegatable` / `is_default` / `name` to real readers) measured the column at zero on both sides: the only row writers — the builtin and declared position bootstrappers — set label / description / managed_by / active / is_default, and position→grant resolution consults `sys_position_permission_set` rows plus the position `name`, never this column. Its only in-repo reference was the clone_position action copying it between rows — a copy of a value nothing writes. objectui was searched under the same discipline (evidenceScope closure): no console surface names the column — the position pickers and Setup views read name / label / id only, so a designer preview consumer does not exist either. That left a declared free-text grant catalogue on a security object that no runtime enforced: an author — human or AI — who filled it believed they granted permission strings directly on the position, and nothing refused or honoured the value. This is a platform-object COLUMN retirement, not a spec-key retirement, so the bookkeeping follows the ups-delegated-from-column-retired shape: nothing lands in RETIRED_KEYS_BY_MAJOR (no authorable spec KEY changed — PositionSchema never declared `permissions`, and the surface ratchets are expected byte-identical), no liveness-ledger row is added (the ledger walks PositionSchema's shape, which never carried the key — a row would be an orphan), and the disposition is a SEMANTIC entry rather than a D2 conversion: no conversion in the chain rewrites seed rows today and the measured author base is zero, while the loud channel already exists at runtime — the engine schema preflight refuses an undeclared field with 400 INVALID_FIELD before the driver or any hook runs — so this entry carries the prescription and the refusal carries the enforcement. The live-authoring half is the PositionSchema strict-parse guidance for `permissions`, which names the binding table in the rejection. ⚠️ Existing physical columns are deliberately untouched: schema sync is additive (ADR-0045), so a deployed database keeps the column; the platform stops declaring, projecting or accepting it. Zero producers means no rows are expected to carry a value; no backfill or destructive DDL is required or wanted. If position-level direct grants ever become a real need, the column is re-declared then, WITH a runtime reader in the same PR — declare-and-enforce or do not declare. @@ -449,15 +449,15 @@ This is a RUNTIME registration API, not stored metadata, so — like `hook-conte - **`tool-requires-confirmation-retired`** — `ai.tool.requiresConfirmation` → put the operation behind an ACTION and set `ai.requiresConfirmation: true` there — the flag the platform confirmation CONTRACT is written against, and that contract is ENFORCED. An AI-facing call on an action declaring the flag must carry the confirmation member `confirm: true` on the request and is REFUSED without it with `ACTION_CONFIRMATION_REQUIRED` (428), the refusal naming the action and the exact member to set. A gate, not a queue: nothing is parked, and a refused call did not run — no record was read and none was written. ⚠ Two bounds: the enforced set is the doors that enforce the author's `ai.exposed` opt-in, today the action door reached from the MCP `run_action` tool, while REST `/actions` is not `ai.exposed`-gated and sits outside the gate; and `confirm: true` is an unverifiable caller claim, so the gate makes forgetting loud without proving a human approved - Why not automatic: `ToolSchema.requiresConfirmation` accepted `true` and no execution path ever read it: not the LLM tool set (a tool reaches the model as name / description / parameters only), not `ToolRegistry.execute`, not `POST /ai/tools/:name/execute`, and not the MCP bridge, which derives `destructiveHint` from a hardcoded name list. Setting it on a destructive tool produced NO PAUSE. For an ordinary dead property that is untidy; for a SAFETY property it is false compliance, the case ADR-0049 exists for — an author gates a destructive tool, sees the flag accepted, and ships believing a human is in the loop. It is made worse by the near-miss: `action.ai.requiresConfirmation` carries the same name and DOES work, so the mistake reads as correct in review. This is registered as a semantic entry rather than a mechanical conversion because the rewrite is not a rename at all — the replacement lives on a different metadata object at a different layer, and deciding which action should carry the gate (or whether the operation should be an action at all) is a judgement the chain cannot make. Deleting the key mechanically would be the worst possible transform here: it would leave the metadata parsing green while silently completing the removal of a safety gate the author believed was in place. `ToolSchema` was made `.strict()` in the same change, which is load-bearing rather than tidying — removing a key from a non-strict schema swaps one silent no-op for another, so the retired key now REJECTS and the parse error carries the prescription, that being the one channel every consumer bumping `@objectstack/spec` is guaranteed to hit. Registered by the #6350 stock reconciliation: the `retiredKey()` tombstone shipped with #3715 and still stands in `ai/tool.zod.ts`, but the ledger half never did. A retirement needs both — the tombstone is the proof the removal was declared, this entry is what `spec-changes.json`, the upgrade guide and `os migrate meta` project to consumers. ADR-0033 §2 / ADR-0049 / ADR-0087, #3715 (backfilled #6350). - Done when: No tool definition carries `requiresConfirmation`; the key now raises a located parse error naming the replacement, so the sweep is "fix until nothing raises". ⚠️ The load-bearing half is what happens NEXT, and no gate can check it for you: for every tool that carried the flag, decide whether that operation genuinely needs a human in the loop. If it does, move it behind an action carrying `ai.requiresConfirmation: true`, which is what the confirmation contract (#16293) gates on — and that gate is PERFORMED: invoking the operation over an AI-exposed door without the confirmation member is REFUSED with `ACTION_CONFIRMATION_REQUIRED` (428) and nothing runs, so that call is a real check you can make rather than a destructive experiment. ⚠ Two bounds on what it proves: the enforced set is the doors that enforce the author's `ai.exposed` opt-in — today the action door reached from the MCP `run_action` tool — while REST `/actions` is not `ai.exposed`-gated and sits outside the gate, so an agent holding an API key on that route is still yours to put a human in front of; and `confirm: true` is an unverifiable caller claim, so the gate makes forgetting loud without proving a human approved. The decision above is still the one this criterion asks you to make. If the operation does not need a human, delete the key knowingly. Deleting it without that decision leaves exactly the state the retirement exists to end: a destructive tool nobody is approving, now without even the false flag to show that somebody once meant to. -- **`ui-interaction-config-family-retired`** — `ui.touchInteraction / ui.gestureConfig / ui.dndConfig / ui.keyboardNavigationConfig / ui.componentAnimation / ui.motionConfig / ui.pageTransition / ui.offlineConfig (the whole export surface of ui/touch.zod.ts, ui/dnd.zod.ts, ui/keyboard.zod.ts, ui/animation.zod.ts and ui/offline.zod.ts — 32 defs, 64 exported names)` → (removed — there is no replacement key, because there was never a key. Touch targets, drag-and-drop, focus management, keyboard shortcuts and motion are RENDERER BUILT-IN behaviour: the component library decides them, not a per-page metadata author. Offline is a platform capability, and its vocabulary belongs on the sync engine that owns the queue, the conflict policy and the cache — none of which exists yet. Delete the import and the value. Whichever of these earns real product pull returns WITH its own vocabulary and its executor, the #4910 way, not by un-retiring a declaration) - - Why not automatic: Five `@objectstack/spec/ui` modules declared a full interaction-configuration vocabulary — 22 `z.object` sites across touch/gesture, drag-and-drop, focus/keyboard, animation/motion and offline/sync — and NOTHING in the protocol carried them. This is the ADR-0049 false-compliance shape in its most inviting form for an AI author (ADR-0033), and worse than the ordinary declared-but-unread defect: `authorable-surface.json` listed 109 keys under these defs and `content/docs/references/ui/{touch,dnd,keyboard,animation,offline}.mdx` rendered them as authoring tables, so the published documentation advertised a vocabulary with no carrier key anywhere. An author following `dnd.mdx` and writing a `dnd:` block onto a page component was rejected by `PageComponentSchema` for an unrecognized key — the docs and the schema disagreeing about the platform (Prime Directive #10). Three independent measurements, each with its controls passing in the same run: (1) no module under `packages/spec/src` imported any of the five except the `ui/index.ts` barrel, so no schema declared a carrier key; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema` (25 roots, 4742 nodes) reached none of the 21 named object shapes, while `PageSchema`, `WebhookSchema` and `StateMachineSchema` all resolved `direct` and a synthetic carrier flipped all 21 — so unreachability was a fact about the graph, not a broken walker; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud outside these modules' own unit tests. objectui holds TYPE re-exports and parity ratchets, never validators, and says so (#2561). The 2026-08-04 ruling weighed wiring a carrier key (option B) and rejected it: that is a feature with a renderer behind it, not ledger clean-up. It also weighed tightening the shapes to `strictObject` and rejected that explicitly — strictness is a property of a PARSE and there is no parse, so it would spend a breaking change to leave "a precisely validated dead slot, the more convincing lie" (#4583). Because there was no carrier key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 conversion to rewrite: this entry is the D3 record, the same route 3 as #4834 (kernel plugin-runtime family) and #4938 (`HttpServerConfig`). ⚠️ Not to be confused with #5021, which retired the THEME `animation` block — a different file, different defs, and that one did have a carrier key and therefore a tombstone. ADR-0049, #4988. +- **`ui-interaction-config-family-retired`** — `ui.touchInteraction / ui.gestureConfig / ui.dndConfig / ui.keyboardNavigationConfig / ui.componentAnimation / ui.motionConfig / ui.pageTransition / ui.offlineConfig (the whole export surface of ui/touch.zod.ts, ui/dnd.zod.ts, ui/keyboard.zod.ts, ui/animation.zod.ts and ui/offline.zod.ts — 32 defs, 64 exported names)` → (removed — there is no replacement key, because there was never a key. Touch targets, drag-and-drop, focus management, keyboard shortcuts and motion are RENDERER BUILT-IN behaviour: the component library decides them, not a per-page metadata author. Offline is a platform capability, and its vocabulary belongs on the sync engine that owns the queue, the conflict policy and the cache — none of which exists yet. Delete the import and the value. Whichever of these earns real product pull returns WITH its own vocabulary and its executor — the way inbound rate limiting came back, as a new key carrying only what its executor consumes — not by un-retiring a declaration) + - Why not automatic: Five `@objectstack/spec/ui` modules declared a full interaction-configuration vocabulary — 22 `z.object` sites across touch/gesture, drag-and-drop, focus/keyboard, animation/motion and offline/sync — and NOTHING in the protocol carried them. This is the ADR-0049 false-compliance shape in its most inviting form for an AI author (ADR-0033), and worse than the ordinary declared-but-unread defect: `authorable-surface.json` listed 109 keys under these defs and `content/docs/references/ui/{touch,dnd,keyboard,animation,offline}.mdx` rendered them as authoring tables, so the published documentation advertised a vocabulary with no carrier key anywhere. An author following `dnd.mdx` and writing a `dnd:` block onto a page component was rejected by `PageComponentSchema` for an unrecognized key — the docs and the schema disagreeing about the platform (Prime Directive #10). Three independent measurements, each with its controls passing in the same run: (1) no module under `packages/spec/src` imported any of the five except the `ui/index.ts` barrel, so no schema declared a carrier key; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema` (25 roots, 4742 nodes) reached none of the 21 named object shapes, while `PageSchema`, `WebhookSchema` and `StateMachineSchema` all resolved `direct` and a synthetic carrier flipped all 21 — so unreachability was a fact about the graph, not a broken walker; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud outside these modules' own unit tests. objectui holds TYPE re-exports and parity ratchets, never validators, and says so (its types package deliberately dropped the spec/ui zod-validator re-exports and keeps type-only ones). The 2026-08-04 ruling retired the family — touch, drag-and-drop, keyboard and motion are renderer built-in behaviour and offline belongs to a sync engine, none of it per-page metadata — and weighed wiring a carrier key (option B) and rejected it: that is a feature with a renderer behind it, not ledger clean-up. It also weighed tightening the shapes to `strictObject` and rejected that explicitly — strictness is a property of a PARSE and there is no parse, so it would spend a breaking change to leave "a precisely validated dead slot, the more convincing lie" (the lesson of the datasource capability flags: `readOnly` was precisely validated and read by nothing, while a shipped example called a datasource a read replica and wrote through it). Because there was no carrier key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 conversion to rewrite: this entry is the D3 record, the same route 3 as `plugin-runtime-family-retired` (the kernel plugin-runtime family) and the `HttpServerConfig` retirement (seven keys no runtime read and no authoring door reached, retired with their container). ⚠️ Not to be confused with the theme-token retirement (theme-driven typography is not a near-term capability, so nine token groups nothing consumed were retired), which retired the THEME `animation` block — a different file, different defs, and that one did have a carrier key and therefore a tombstone. ADR-0049. - Done when: No code imports any of the 64 retired names from `@objectstack/spec` or `@objectstack/spec/ui` — `TouchTargetConfig(Schema)`, `GestureType(Schema)`, `SwipeDirection(Schema)`, `SwipeGestureConfig(Schema)`, `PinchGestureConfig(Schema)`, `LongPressGestureConfig(Schema)`, `GestureConfig(Schema)`, `TouchInteraction(Schema)`, `TransitionPreset(Schema)`, `EasingFunction(Schema)`, `TransitionConfig(Schema)`, `AnimationTrigger(Schema)`, `ComponentAnimation(Schema)`, `PageTransition(Schema)`, `MotionConfig(Schema)`, `DragHandle(Schema)`, `DropEffect(Schema)`, `DragConstraint(Schema)`, `DropZone(Schema)`, `DragItem(Schema)`, `DndConfig(Schema)`, `FocusTrapConfig(Schema)`, `KeyboardShortcut(Schema)`, `FocusManagement(Schema)`, `KeyboardNavigationConfig(Schema)`, `OfflineStrategy(Schema)`, `ConflictResolution(Schema)`, `SyncConfig(Schema)`, `PersistStorage(Schema)`, `EvictionPolicy(Schema)`, `OfflineCacheConfig(Schema)`, `OfflineConfig(Schema)` — every one is TS2305 after upgrade, on every public entry (pinned by resolved symbol identity in `ui/interaction-config-retirement.test.ts`). No metadata document needs editing, because none could ever carry one of these blocks: a stack that parsed before parses byte-for-byte the same after. If you consumed the bare `ConflictResolution` from `@objectstack/spec/ui` as a TYPE for your own offline code, declare that union locally — it is your client's policy, not the platform's. `@objectstack/spec/integration`'s `ConnectorConflictResolution` (connector sync) and `@objectstack/spec/api`'s `ConflictResolutionStrategy` (route merge policy) are different concepts and are untouched. - **`ui-notification-action-embed-config-retired`** — `ui.notificationAction / ui.embedConfig` → (removed — there is no replacement shape, because there was never a key to write either into. Delete the import and the value. Notification presentation is still described by the surviving `NotificationType` / `NotificationSeverity` / `NotificationPosition` vocabulary; public access to a form is granted by the LIVE `FormView.sharing` block (`SharingConfig`), which is untouched. Notification action buttons as metadata, and iframe embedding, return via the enforce route of ADR-0049 through a new ADR — carrier key and renderer first, vocabulary second) - - Why not automatic: Both shapes were published `@objectstack/spec/ui` vocabulary with NO AUTHORING DOOR. #4001 批 14 measured them three ways on 2026-08-03 and this retirement re-ran all three against `origin/main` before removing anything, each with a positive control that passed in the same run: (1) CARRIER — no schema in `packages/spec/src` declared a key of either type (`ui/notification.zod`'s only non-test importer was the barrel; `ui/sharing.zod`'s were the barrel and `ui/view.zod.ts`, which names its SIBLING `SharingConfigSchema`), measured by resolving specifiers rather than substring-matching, because the repo holds two `sharing.zod` modules and a substring test miscredits `stack.zod.ts` to the UI one; (2) REACHABILITY — a BFS from the 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema`, over `build-schemas.ts`'s own walk including its derived-clone bridge, never reached either, while `Page` / `Action` / `DashboardWidget` / `Webhook` and `SharingConfig` itself all resolved `root-graph` in the same run and an injected synthetic carrier flipped both; (3) PARSE — zero `.parse()` in objectstack, cloud or objectui outside their own unit tests. So nobody could author one and nothing ever validated one: the #3950 shape, an exported schema with no consumer read as a capability, and the ADR-0033 trap where an AI author takes `EmbedConfigSchema` in the published bundle as proof the platform serves iframes. Neither is stored metadata and neither has a carrier, so no `sys_metadata` row can hold one and there is no source for the D2 chain to rewrite; this entry is the D3 record. 批 14 deliberately did NOT close them with `.strict()` — strictness is a property of a PARSE, and closing a shape nothing parses buys only "a precisely-validated dead slot, the more convincing lie" (#4583) — and filed the disposition as #5015, ruled REMOVE on 2026-08-04. Each was orphaned by an earlier retirement one level up: `NotificationAction` lost its wrappers at #4610 (`NotificationSchema` / `NotificationConfigSchema`, the #4535 C3 dual-source cleanup — that retirement's published "zero consumers" evidence was later falsified for objectui and is corrected on `ui/notification.zod`'s tombstone; the removal itself stands, #5781), and `EmbedConfig` lost its key at 17.0.0 when the 2026-06 liveness audit retired `App.embed` (no iframe route ever read it) — that key still stands as a `retiredKey()` tombstone in `app.zod.ts`, so an author who wrote the KEY already meets a prescription; this removes the value shape that outlived it. ⚠️ The retirement is per SCHEMA, not per file: `ui/sharing.zod` KEEPS `SharingConfigSchema`, a live door carried by `FormViewSchema.sharing` and read by `rest-server.ts` to mount the anonymous form routes, and `ui/notification.zod` keeps its three presentation enums. objectui consumed `NotificationActionSchema.shape.variant` as a VOCABULARY (never a parse) to pin its own hand-written `NotificationActionButton` interface — which is exactly why "has a consumer" never meant "has an authoring door" here; that pin is adapted objectui-side when it refreshes this dependency. ADR-0049, #5015. + - Why not automatic: Both shapes were published `@objectstack/spec/ui` vocabulary with NO AUTHORING DOOR. The v17 unknown-key strictness sweep (its ui/ batch 14), which measured each ui/ file for an authoring door before closing any shape, measured them three ways on 2026-08-03 and this retirement re-ran all three against `origin/main` before removing anything, each with a positive control that passed in the same run: (1) CARRIER — no schema in `packages/spec/src` declared a key of either type (`ui/notification.zod`'s only non-test importer was the barrel; `ui/sharing.zod`'s were the barrel and `ui/view.zod.ts`, which names its SIBLING `SharingConfigSchema`), measured by resolving specifiers rather than substring-matching, because the repo holds two `sharing.zod` modules and a substring test miscredits `stack.zod.ts` to the UI one; (2) REACHABILITY — a BFS from the 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema`, over `build-schemas.ts`'s own walk including its derived-clone bridge, never reached either, while `Page` / `Action` / `DashboardWidget` / `Webhook` and `SharingConfig` itself all resolved `root-graph` in the same run and an injected synthetic carrier flipped both; (3) PARSE — zero `.parse()` in objectstack, cloud or objectui outside their own unit tests. So nobody could author one and nothing ever validated one: the shape of the unwired plugin sandboxing config removed before it, an exported schema with no consumer read as a capability, and the ADR-0033 trap where an AI author takes `EmbedConfigSchema` in the published bundle as proof the platform serves iframes. Neither is stored metadata and neither has a carrier, so no `sys_metadata` row can hold one and there is no source for the D2 chain to rewrite; this entry is the D3 record. That batch deliberately did NOT close them with `.strict()` — strictness is a property of a PARSE, and closing a shape nothing parses buys only "a precisely-validated dead slot, the more convincing lie" (the lesson of the datasource capability flags, whose `readOnly` was precisely validated and read by nothing) — and left the disposition to ADR-0049's enforce-or-remove, which came back REMOVE on 2026-08-04: a dead surface with no authoring door retires implementation-first, as three same-shape rulings that week had already decided. Each was orphaned by an earlier retirement one level up: `NotificationAction` lost its wrappers when the dual-source cleanup removed the `./ui` copies of `NotificationSchema` / `NotificationConfigSchema` (the same names declared differently on other entry points) — that retirement's published "zero consumers" evidence was later falsified for objectui, which re-exported both names, and is corrected on `ui/notification.zod`'s tombstone; the removal itself stands — and `EmbedConfig` lost its key at 17.0.0 when the 2026-06 liveness audit retired `App.embed` (no iframe route ever read it) — that key still stands as a `retiredKey()` tombstone in `app.zod.ts`, so an author who wrote the KEY already meets a prescription; this removes the value shape that outlived it. ⚠️ The retirement is per SCHEMA, not per file: `ui/sharing.zod` KEEPS `SharingConfigSchema`, a live door carried by `FormViewSchema.sharing` and read by `rest-server.ts` to mount the anonymous form routes, and `ui/notification.zod` keeps its three presentation enums. objectui consumed `NotificationActionSchema.shape.variant` as a VOCABULARY (never a parse) to pin its own hand-written `NotificationActionButton` interface — which is exactly why "has a consumer" never meant "has an authoring door" here; that pin is adapted objectui-side when it refreshes this dependency. ADR-0049. - Done when: No code imports `NotificationActionSchema`, `NotificationAction`, `EmbedConfigSchema` or `EmbedConfig` from `@objectstack/spec` or `@objectstack/spec/ui` — both are TS2305 after upgrade, on every public entry (pinned by resolved symbol identity in `notification-embed-retirement.test.ts`). The same pin asserts the SURVIVORS in the same run, and that half is equally load-bearing: `NotificationTypeSchema` / `NotificationSeveritySchema` / `NotificationPositionSchema` and `SharingConfigSchema` must still be exported from `./ui`, and both modules must still load — a retirement that deleted either file would satisfy the absence half while destroying working surface. Nothing regresses at runtime, because nothing ever ran: no notification action was ever parsed from metadata and no iframe route ever read an embed config. Public form sharing is unaffected — `FormView.sharing` still gates the anonymous endpoints on `allowAnonymous` + `publicLink`. -- **`ui-widget-i18n-family-retired`** — `ui.widgetManifest / ui.widgetLifecycle / ui.widgetEvent / ui.widgetProperty / ui.widgetSource / ui.i18nObject / ui.pluralRule / ui.numberFormat / ui.dateFormat / ui.localeConfig (the widget-registration vocabulary of ui/widget.zod.ts, and the five doorless shapes of ui/i18n.zod.ts — 10 defs, 26 exported names)` → (removed — there is no replacement key, because there was never a key. A custom field widget is still named the same way it always was: `field.widget` is a plain string naming a component the RENDERER has registered, and objectui's registry has always carried its own runtime manifest for that (`RuntimeWidgetManifest` / `RuntimeWidgetSource` in `@object-ui/types`, objectui#3161 / #4115), which models different keys and never derived from these. For localisation: write the default-language string on `label` / `description` — the framework generates the translation key at registration time from the naming convention — and put translations in translation files, which is the LIVE `system/translation.zod.ts` surface. Widget registration and locale formatting as authorable protocol metadata return via the ENFORCE route of ADR-0049 through a new ADR — the registry / loader / formatter first, the vocabulary second) - - Why not automatic: `ui/widget.zod.ts` published a complete widget-registration vocabulary — a manifest with lifecycle hooks, custom events, configurable properties and an npm/remote/inline implementation-source union — and `ui/i18n.zod.ts` published a structured-label, plural-rule and locale-formatting vocabulary. NOTHING in the protocol carried either. Three independent measurements, re-run on `origin/main` immediately before the removal with their controls passing in the SAME run: (1) no module under `packages/spec/src` imported `widget.zod` at all, and the only imports of `i18n.zod` anywhere name `I18nLabelSchema` / `AriaPropsSchema` (both KEPT), so no schema declared a carrier key — `field.widget` is a `z.string()` naming a registered component and has never referenced `WidgetManifest`; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema` reached none of them, while `PageSchema` / `ObjectListViewSchema` resolved `direct` in the same run and a synthetic carrier flipped every one of them; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud outside these files' own unit tests. `NumberFormat` / `DateFormat` DID have a carrier key (`LocaleConfig.numberFormat` / `.dateFormat`) but the carrier was itself doorless, so the subtree was `no door` rather than `no gate` and goes whole — leaving the two leaves behind would strand exported schemas with no consumer (#3950). `I18nObjectSchema` was additionally superseded by its own file-neighbour: `I18nLabelSchema`'s documentation already says translation keys are generated at registration time and translations live in translation files, and the live translation surface is `system/translation.zod.ts`, which uses none of these shapes. The 2026-08-06 ruling weighed giving them a carrier (option B) and rejected it: that is a feature with a registry and a renderer behind it, not ledger clean-up. Tightening them to `strictObject` was rejected earlier and explicitly (#4001 批 16) — strictness is a property of a PARSE and there is no parse, so it would spend a breaking change to leave "a precisely validated dead slot, the more convincing lie" (#4583). With no carrier key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 conversion to rewrite: this entry is the D3 record, route 3, the same shape as #4988 (the ui/ interaction config family), #4834 (kernel plugin-runtime family) and #4938 (`HttpServerConfig`). ⚠️ `WidgetManifest.performance`'s own `retiredKey()` tombstone (#3896 close-out) is SUBSUMED here, the #4657/#4834 way: it goes with the shape that carried it, which is strictly stronger than the tombstone, because there is no longer a manifest to author the key INTO. ⚠️ One of the nine widget sites is deliberately NOT retired. `FieldWidgetPropsSchema` survives: it is a REACT PROPS CONTRACT rather than authorable metadata (it never appeared in `authorable-surface/` or `json-schema.manifest/` — its `onChange` is a `z.function()`), so "zero parse" is its design and not its defect, and it acquired a live cross-repo compile-time consumer one day before 批 16 measured: objectui PR #3289 (2026-08-03) renamed `@object-ui/fields`' validation slot onto the spec's `error` with no alias, the form renderer began producing it, and `packages/fields/src/__tests__/spec-symbol-batch7.test.ts` pins the shape against `import type { FieldWidgetProps } from '@objectstack/spec/ui'` as an intentional tripwire. Re-verified on objectui `origin/main` 2026-08-07. ADR-0049, #5055. - - Done when: No code imports `WidgetManifest(Schema|Parsed)`, `WidgetLifecycle(Schema)`, `WidgetEvent(Schema|Parsed)`, `WidgetProperty(Schema|Parsed)`, `WidgetSource(Schema|Parsed)`, `I18nObject(Schema)`, `PluralRule(Schema)`, `NumberFormat(Schema|Parsed)`, `DateFormat(Schema)` or `LocaleConfig(Schema|Parsed)` from `@objectstack/spec` or `@objectstack/spec/ui` — every one is TS2305 after upgrade, on every public entry (pinned by resolved symbol identity in `ui/widget-i18n-retirement.test.ts`). No metadata document needs editing, because none could ever carry one of these shapes: a stack that parsed before parses byte-for-byte the same after, and a `field.widget: "my_picker"` string is untouched. `FieldWidgetProps` / `FieldWidgetPropsSchema` / `FieldWidgetPropsParsed`, `I18nLabel(Schema)` and `AriaProps(Schema)` all still resolve on `@objectstack/spec/ui` and are asserted to. ⚠️ objectui needs a companion PR in the same window: `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts` asserts the spec STILL owns `WidgetManifest` / `WidgetSource` (it is the "a workaround should not outlive its reason" half of the objectui#3169 tripwire, designed to go red exactly here), and `packages/types/src/widget.ts`'s "Renamed off the spec's `WidgetManifest` name" comments now point at names that no longer exist. Both are prescribed responses to this removal, not collateral damage. +- **`ui-widget-i18n-family-retired`** — `ui.widgetManifest / ui.widgetLifecycle / ui.widgetEvent / ui.widgetProperty / ui.widgetSource / ui.i18nObject / ui.pluralRule / ui.numberFormat / ui.dateFormat / ui.localeConfig (the widget-registration vocabulary of ui/widget.zod.ts, and the five doorless shapes of ui/i18n.zod.ts — 10 defs, 26 exported names)` → (removed — there is no replacement key, because there was never a key. A custom field widget is still named the same way it always was: `field.widget` is a plain string naming a component the RENDERER has registered, and objectui's registry has always carried its own runtime manifest for that (`RuntimeWidgetManifest` / `RuntimeWidgetSource` in `@object-ui/types`, renamed off the spec's names under objectui's rule that a symbol named like a spec export must import it or take a name of its own), which models different keys and never derived from these. For localisation: write the default-language string on `label` / `description` — the framework generates the translation key at registration time from the naming convention — and put translations in translation files, which is the LIVE `system/translation.zod.ts` surface. Widget registration and locale formatting as authorable protocol metadata return via the ENFORCE route of ADR-0049 through a new ADR — the registry / loader / formatter first, the vocabulary second) + - Why not automatic: `ui/widget.zod.ts` published a complete widget-registration vocabulary — a manifest with lifecycle hooks, custom events, configurable properties and an npm/remote/inline implementation-source union — and `ui/i18n.zod.ts` published a structured-label, plural-rule and locale-formatting vocabulary. NOTHING in the protocol carried either. Three independent measurements, re-run on `origin/main` immediately before the removal with their controls passing in the SAME run: (1) no module under `packages/spec/src` imported `widget.zod` at all, and the only imports of `i18n.zod` anywhere name `I18nLabelSchema` / `AriaPropsSchema` (both KEPT), so no schema declared a carrier key — `field.widget` is a `z.string()` naming a registered component and has never referenced `WidgetManifest`; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema` reached none of them, while `PageSchema` / `ObjectListViewSchema` resolved `direct` in the same run and a synthetic carrier flipped every one of them; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud outside these files' own unit tests. `NumberFormat` / `DateFormat` DID have a carrier key (`LocaleConfig.numberFormat` / `.dateFormat`) but the carrier was itself doorless, so the subtree was `no door` rather than `no gate` and goes whole — leaving the two leaves behind would strand exported schemas with no consumer, which an author reads as a capability. `I18nObjectSchema` was additionally superseded by its own file-neighbour: `I18nLabelSchema`'s documentation already says translation keys are generated at registration time and translations live in translation files, and the live translation surface is `system/translation.zod.ts`, which uses none of these shapes. The 2026-08-06 ruling weighed giving them a carrier (option B) and rejected it: that is a feature with a registry and a renderer behind it, not ledger clean-up. Tightening them to `strictObject` was rejected earlier and explicitly, by the batch of the v17 unknown-key strictness sweep that measured this file as having no authoring door — strictness is a property of a PARSE and there is no parse, so it would spend a breaking change to leave "a precisely validated dead slot, the more convincing lie" (the lesson of the datasource capability flags, whose `readOnly` was precisely validated and read by nothing). With no carrier key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 conversion to rewrite: this entry is the D3 record, route 3, the same shape as `ui-interaction-config-family-retired`, `plugin-runtime-family-retired` and the `HttpServerConfig` retirement. ⚠️ `WidgetManifest.performance`'s own `retiredKey()` tombstone (left by the close-out sweep that removed the inert `performance` keys no renderer applied) is SUBSUMED here, the way the kernel `activationEvents` tombstone went with the removed plugin-runtime family: it goes with the shape that carried it, which is strictly stronger than the tombstone, because there is no longer a manifest to author the key INTO. ⚠️ One of the nine widget sites is deliberately NOT retired. `FieldWidgetPropsSchema` survives: it is a REACT PROPS CONTRACT rather than authorable metadata (it never appeared in `authorable-surface/` or `json-schema.manifest/` — its `onChange` is a `z.function()`), so "zero parse" is its design and not its defect, and it acquired a live cross-repo compile-time consumer one day before that sweep batch measured: an objectui fix of 2026-08-03, made to follow the spec, renamed `@object-ui/fields`' validation slot onto the spec's `error` with no alias, the form renderer began producing it, and `packages/fields/src/__tests__/spec-symbol-batch7.test.ts` pins the shape against `import type { FieldWidgetProps } from '@objectstack/spec/ui'` as an intentional tripwire. Re-verified on objectui `origin/main` 2026-08-07. ADR-0049. + - Done when: No code imports `WidgetManifest(Schema|Parsed)`, `WidgetLifecycle(Schema)`, `WidgetEvent(Schema|Parsed)`, `WidgetProperty(Schema|Parsed)`, `WidgetSource(Schema|Parsed)`, `I18nObject(Schema)`, `PluralRule(Schema)`, `NumberFormat(Schema|Parsed)`, `DateFormat(Schema)` or `LocaleConfig(Schema|Parsed)` from `@objectstack/spec` or `@objectstack/spec/ui` — every one is TS2305 after upgrade, on every public entry (pinned by resolved symbol identity in `ui/widget-i18n-retirement.test.ts`). No metadata document needs editing, because none could ever carry one of these shapes: a stack that parsed before parses byte-for-byte the same after, and a `field.widget: "my_picker"` string is untouched. `FieldWidgetProps` / `FieldWidgetPropsSchema` / `FieldWidgetPropsParsed`, `I18nLabel(Schema)` and `AriaProps(Schema)` all still resolve on `@objectstack/spec/ui` and are asserted to. ⚠️ objectui needs a companion PR in the same window: `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts` asserts the spec STILL owns `WidgetManifest` / `WidgetSource` (it is the "a workaround should not outlive its reason" half of the rename tripwire objectui added when it stopped declaring symbols under names the spec owns, designed to go red exactly here), and `packages/types/src/widget.ts`'s "Renamed off the spec's `WidgetManifest` name" comments now point at names that no longer exist. Both are prescribed responses to this removal, not collateral damage. - **`ups-delegated-from-column-retired`** — `sys_user_permission_set.delegated_from — the ADR-0091 D3 provenance column left the platform grant table declared by plugin-security (packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts). The sibling declaration on sys_user_position is untouched` → nothing on this table — delete the key from any authored `sys_user_permission_set` seed row (stack `data` entries) or data-door write that still carries it. Delegation semantics live on `sys_user_position`, where `delegated_from` remains declared AND runtime-enforced: the delegated-admin gate is what makes a position insert a delegation, and the explain engine attributes "via delegation from X, until Y". A permission-set grant that needs a provenance note keeps `reason` (free text), which remains declared on both grant tables - Why not automatic: Maintainer ruling 2026-08-18 (#9730), ADR-0049 enforce-or-remove: REMOVE. The runtime delegation gate is structurally scoped to sys_user_position (`isDelegationWrite` returns false for every other object, so `assertSelfDelegation` is unreachable for this table), and the explain engine reads delegation provenance from sys_user_position rows only. On sys_user_permission_set the column was therefore declared and data-door-writable while NO runtime consumer read it — its only enforcement was an authoring-time lint (the D3 "delegation row needs a reason" rule), which a row written through the generic data door never meets. That is declared-but-unenforced in its pure form, on a security object: an author who stamped delegated_from on a permission-set grant believed they constrained delegation, and nothing refused or honoured it. Producers measured at zero — the only object literals naming both the table and the column were lint test fixtures. This is a platform-object COLUMN retirement, not a spec-key retirement, so the bookkeeping follows the audit-log-action-enum-retired shape: nothing lands in RETIRED_KEYS_BY_MAJOR (no authorable spec KEY changed — the surface ratchets are expected byte-identical), and the disposition is a SEMANTIC entry rather than a D2 conversion. A conversion over stack `data` seed records would be mechanically expressible, but no conversion in the chain rewrites seed rows today and the measured author base is zero; the loud channel already exists at runtime — the engine schema preflight refuses an undeclared field with 400 INVALID_FIELD before the driver or any hook runs — so this entry carries the prescription and the refusal carries the enforcement. ⚠️ Existing physical columns are deliberately untouched: schema sync is additive (ADR-0045), so a deployed database keeps the column; the platform stops declaring, projecting or accepting it. Zero producers means no rows are expected to carry a value; no backfill or destructive DDL is required or wanted. If delegation at permission-set granularity ever becomes a real need, the column is re-declared then, WITH a runtime reader in the same PR — declare-and-enforce or do not declare. - Done when: No authored stack seeds `delegated_from` on a sys_user_permission_set record, and no client write to that table carries the key. Concretely: (1) grep your stack sources for delegated_from next to sys_user_permission_set — delete the key from any seed row; a row that was recording genuine hand-over provenance should say it in `reason` instead, which the platform stores on both grant tables. (2) Boot and load your stack: a missed seed row fails loudly at insert with 400 INVALID_FIELD naming the column — that refusal is the enforced channel, not a silent drop. (3) If you meant actual delegation-of-duty, author it where it is enforced: a sys_user_position insert with delegated_from = the writer, a mandatory future valid_until within the ceiling, and a mandatory reason (ADR-0091 D3) — the delegated-admin gate then validates the whole shape at runtime. diff --git a/packages/cli/test/migrate-meta-engine-guidance.test.ts b/packages/cli/test/migrate-meta-engine-guidance.test.ts index 818380465b7..57f0671b2f0 100644 --- a/packages/cli/test/migrate-meta-engine-guidance.test.ts +++ b/packages/cli/test/migrate-meta-engine-guidance.test.ts @@ -1,8 +1,9 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * `os migrate meta` — the guidance it prints for the `engine-*` ADR-0087 - * semantic entries states each lesson in words and carries no tracker number. + * `os migrate meta` — the guidance it prints for the ADR-0087 semantic entries + * of the COVERED families (`engine-*`, `ui-*`, `plugin-*`) states each lesson + * in words and carries no tracker number. * * ## What this pins * @@ -12,21 +13,26 @@ * author is shown, so it carries no tracker number: a number sends the reader * to a page that can be deleted (some cited pages already had been), and the * lesson the entry exists to teach then sits behind a dead link instead of in - * the sentence being read. The `engine-*` entries were rewritten to say what - * each cited ruling, measurement or fix decided; ADR ids stay, because an ADR - * lives in this repository. + * the sentence being read. The covered families were rewritten, one staged + * family at a time, to say what each cited ruling, measurement or fix decided; + * ADR ids stay, because an ADR lives in this repository. The whole printed + * block is held, so `surface` is held as well as the three prose fields. * - * The fixture authors the shapes those entries are about — a lookup and a - * virtual `formula` field on one object — and the CLI replays the chain from - * the support floor to the highest major carrying an `engine-*` entry. Each - * family block is then located VERBATIM in what the terminal printed, and that - * printed block must hold no `#` followed by four or five digits. + * The chain reports every semantic entry of every hop it crosses, whatever the + * stack authors, so the fixture only has to be a real stack the command loads; + * it keeps the lookup and the virtual `formula` field the `engine-*` entries + * are about. The CLI replays the chain from the support floor to the highest + * major carrying a covered entry. Each covered block is then located VERBATIM + * in what the terminal printed, and that printed block must hold no `#` + * followed by four or five digits. The file keeps the name it was given when + * `engine-*` was the only covered family. * * ## Why it cannot pass by reading nothing * - * - The family is derived from the registry by id prefix, so an `engine-*` - * entry added later is held to the same line on arrival — and the derived set - * must still contain the five entries this rewrite covered, so an emptied + * - The covered set is derived from the registry by id prefix, so an entry + * added later to a covered family is held to the same line on arrival — and + * the derived set must still contain every entry the rewrites covered, and + * every covered prefix must still select at least one entry, so an emptied * prefix cannot turn every assertion below into a loop over nothing. * - Each block is asserted PRESENT in stdout before it is asserted clean, so a * renderer change that stopped printing the prose fails here instead of @@ -65,16 +71,44 @@ const TSX = resolve(HERE, '../../../node_modules/.bin/tsx'); /** A tracker id as author-shown prose must not carry it: `#` and four or five digits. */ const TRACKER_ID = /#\d{4,5}\b/; -/** The family this pin holds, selected by entry-id prefix. */ -const FAMILY_PREFIX = 'engine-'; +/** The families this pin holds, selected by entry-id prefix. */ +const COVERED_PREFIXES = ['engine-', 'ui-', 'plugin-']; -/** The entries rewritten when the family was brought to this line — the anti-vacuity floor. */ +/** + * The entries rewritten when each family was brought to this line — the + * anti-vacuity floor. A covered entry that carried no tracker id to begin with + * is held by its prefix and needs no row here. + */ const REWRITTEN = [ 'engine-dotted-filter-refused', 'engine-dotted-projection-refused', 'engine-find-formula-filter-refused', 'engine-find-formula-order-by-refused', 'engine-update-upsert-retired', + 'plugin-activation-events-retired', + 'plugin-auto-restart-never-reinitialised', + 'plugin-manifest-contributes-dead-members-retired', + 'plugin-manifest-contributes-routes-retired', + 'plugin-manifest-dead-containers-retired', + 'plugin-manifest-kind-globs-retired', + 'plugin-manifest-loading-retired', + 'plugin-runtime-family-retired', + 'plugin-security-scan-result-surface-retired', + 'plugin-security-scanner-retired', + 'ui-cloud-connection-widgets-unknown-keys-refused', + 'ui-form-field-length-malformed-refused', + 'ui-form-field-precision-scale-integer-refused', + 'ui-form-view-predicate-features-root-refused', + 'ui-interaction-config-family-retired', + 'ui-list-view-groupbyfield-padded-refused', + 'ui-list-view-grouping-field-padded-refused', + 'ui-mcp-connect-agent-unknown-keys-refused', + 'ui-notification-action-embed-config-retired', + 'ui-object-grid-page-size-positive-integer-refused', + 'ui-react-list-view-binding-aliases-retired', + 'ui-record-blocks-unknown-keys-refused', + 'ui-reference-rail-unknown-keys-refused', + 'ui-widget-i18n-family-retired', ]; interface FamilyEntry { @@ -88,7 +122,7 @@ interface FamilyEntry { const FAMILY: FamilyEntry[] = Object.entries(MIGRATIONS_BY_MAJOR).flatMap(([major, step]) => step.semantic - .filter((s) => s.id.startsWith(FAMILY_PREFIX)) + .filter((s) => COVERED_PREFIXES.some((prefix) => s.id.startsWith(prefix))) .map((s) => ({ ...s, toMajor: Number(major) })), ); @@ -102,9 +136,11 @@ function printedBlock(e: FamilyEntry): string { } /** - * A stack authoring the shapes the family's entries are about: a relation a - * dotted path would follow, and a virtual `formula` field no driver - * materialises a column for. + * A real stack for the command to load. It keeps the shapes the `engine-*` + * entries are about — a relation a dotted path would follow, and a virtual + * `formula` field no driver materialises a column for — though which blocks + * print does not depend on it: every semantic entry of a crossed hop is + * reported. */ const FAMILY_FIXTURE = ` export default { @@ -144,7 +180,7 @@ afterAll(() => { try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ } }); -describe('os migrate meta — the engine-* guidance carries no tracker number', () => { +describe('os migrate meta — the guidance of the covered families carries no tracker number', () => { it('the detector fires on a tracker id and stays dark on every other number shape', () => { expect(TRACKER_ID.test(`see #${'9'.repeat(4)}`)).toBe(true); expect(TRACKER_ID.test(`see #${'9'.repeat(5)}`)).toBe(true); @@ -153,12 +189,15 @@ describe('os migrate meta — the engine-* guidance carries no tracker number', expect(TRACKER_ID.test('ADR-0112')).toBe(false); }); - it('selects the whole family, including every entry the rewrite covered', () => { + it('selects every covered family, including every entry the rewrites covered', () => { const ids = FAMILY.map((e) => e.id); + for (const prefix of COVERED_PREFIXES) { + expect(ids.some((id) => id.startsWith(prefix)), `no entry selected for ${prefix}`).toBe(true); + } for (const id of REWRITTEN) expect(ids, `family lost ${id}`).toContain(id); }); - it('prints every family block verbatim, and no printed block names a tracker id', () => { + it('prints every covered block verbatim, and no printed block names a tracker id', () => { for (const e of FAMILY) { const block = printedBlock(e); expect(stdout.includes(block), `${e.id}: its block is not in the printed output`).toBe(true); diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index 147a6da4979..e281653da0b 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -714,21 +714,21 @@ "replacement": "(removed — delete the key. Every plugin activates immediately on load/registration, which is the only behaviour that has ever existed; `activate()` still runs at registration time. Lazy activation, if built, returns via the enforce route of ADR-0049 through a new ADR, with a vocabulary its executor actually honours)", "migrationId": "plugin-activation-events-retired", "toMajor": 17, - "rationale": "Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary they embedded (`onCommand` / `onRoute` / … / `onView` after the #4653 convergence) — promised lazy plugin activation (\"plugins remain dormant until an activation event fires\") that no runtime in objectstack, cloud, cloud-v1 or objectui ever implemented: nothing anywhere read the key, every plugin activates immediately, and cloud-v1's own ROADMAP recorded lazy activation as unimplemented (planned v0.4.0). That is the ADR-0049 false-compliance shape in the semantically-lying direction: an author writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` expected deferral and got eager activation with a clean parse. Neither parent shape is stored metadata — `StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin` (a root schema, never part of a stack tree) and `DynamicLoadRequest` is a runtime request shape with no caller — so no `sys_metadata` row can carry the key and there is no source for the D2 chain to rewrite; this entry is the D3 record. The kernel key is tombstoned via `retiredKey()` (its schema is not `.strict()`; a plain delete would strip an authored value silently), the studio key is rejected by the strict manifest parse with a guidance prescription (as are its former VS Code-flavoured aliases `activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / `ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys (#3950: an exported schema with no consumer is read as a capability). #4657. SUPERSEDED ON THE KERNEL SIDE by #4834 (same unreleased major): the whole `DynamicLoadRequest` shape — and the rest of the plugin-runtime family with it — was removed, which took this key's `retiredKey()` tombstone with it. That is strictly stronger than the tombstone, not weaker: there is no longer a `DynamicLoadRequest` to author the key INTO, so the prescription an author needs is no longer \"delete this key\" but \"this request shape does not exist\" (see `plugin-runtime-family-retired`). The studio half of this entry is unaffected and still enforced by the strict manifest parse." + "rationale": "Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary they embedded (`onCommand` / `onRoute` / … / `onView`, once the kernel and studio copies had converged on the kernel's structured `{ type, pattern }` shape) — promised lazy plugin activation (\"plugins remain dormant until an activation event fires\") that no runtime in objectstack, cloud, cloud-v1 or objectui ever implemented: nothing anywhere read the key, every plugin activates immediately, and cloud-v1's own ROADMAP recorded lazy activation as unimplemented (planned v0.4.0). That is the ADR-0049 false-compliance shape in the semantically-lying direction: an author writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` expected deferral and got eager activation with a clean parse. Neither parent shape is stored metadata — `StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin` (a root schema, never part of a stack tree) and `DynamicLoadRequest` is a runtime request shape with no caller — so no `sys_metadata` row can carry the key and there is no source for the D2 chain to rewrite; this entry is the D3 record. The kernel key is tombstoned via `retiredKey()` (its schema is not `.strict()`; a plain delete would strip an authored value silently), the studio key is rejected by the strict manifest parse with a guidance prescription (as are its former VS Code-flavoured aliases `activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / `ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys (the lesson of the unwired plugin sandboxing / integrity / approval config removed before this: an exported schema with no consumer is read as a capability). Both keys took ADR-0049's REMOVE answer, not ENFORCE, while protocol 17 was still unreleased. SUPERSEDED ON THE KERNEL SIDE by the maintainer's REMOVE ruling on the rest of the plugin-runtime family (same unreleased major): the whole `DynamicLoadRequest` shape — and the rest of the plugin-runtime family with it — was removed, which took this key's `retiredKey()` tombstone with it. That is strictly stronger than the tombstone, not weaker: there is no longer a `DynamicLoadRequest` to author the key INTO, so the prescription an author needs is no longer \"delete this key\" but \"this request shape does not exist\" (see `plugin-runtime-family-retired`). The studio half of this entry is unaffected and still enforced by the strict manifest parse." }, { "surface": "manifest.loading (the whole block: strategy / preload / codeSplitting / dynamicImport / initialization / dependencyResolution / hotReload / caching / sandboxing / monitoring)", "replacement": "nothing to re-declare — delete the key. Plugins are composed at boot: `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder` in `packages/core/src/plugin-order.ts`). For the isolation `loading.sandboxing` appeared to configure, note that the plugin trust tier (`manifest.runtime`, ADR-0025 §3.6) does not supply it either: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the manifest permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation", "migrationId": "plugin-manifest-loading-retired", "toMajor": 17, - "rationale": "ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a complete plugin loading policy and NOTHING read it. A bare-name scan of all three repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), each with a control probe proving the scan saw the tree — put every hit inside `packages/spec` itself: this module's own declaration, its own unit tests, the `Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key parsed, entered the manifest, and changed nothing — #3950, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform isolates plugins, wrote the config, and received a clean parse and zero isolation. An inert security control is worse than an absent one because it is believed. Hot reload was additionally a TWO-SOURCE defect: the docs pointed at this dead `PluginHotReloadSchema` while the only implementation body, `HotReloadManager` (`packages/core/src/hot-reload.ts`), reads a different vocabulary — `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`. Ruling §2 converges on the surviving side: that schema is KEPT as the starting point for a future enforce decision (it has an implementation body but no runtime composes it yet), and enforcing it is deliberately a separate decision, not this retirement. Why D3 semantic and not a D2 conversion: the chain walks a normalized STACK and `applyConversionsToStoredItem` maps a metadata type onto one of its collections. A package manifest is neither — `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a stack collection member and a stored manifest row passes that seam through unchanged. A conversion would be a transform with no seam that ever runs." + "rationale": "ADR-0049 enforce-or-remove; the maintainer ruled REMOVE on 2026-08-04, on condition that a bare-name sweep of cloud and objectui came back clean first. The block declared a complete plugin loading policy and NOTHING read it. A bare-name scan of all three repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), each with a control probe proving the scan saw the tree — put every hit inside `packages/spec` itself: this module's own declaration, its own unit tests, the `Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key parsed, entered the manifest, and changed nothing — an exported schema with no consumer, read as a capability, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform isolates plugins, wrote the config, and received a clean parse and zero isolation. An inert security control is worse than an absent one because it is believed. Hot reload was additionally a TWO-SOURCE defect: the docs pointed at this dead `PluginHotReloadSchema` while the only implementation body, `HotReloadManager` (`packages/core/src/hot-reload.ts`), reads a different vocabulary — `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`. Ruling §2 converges on the surviving side: that schema is KEPT as the starting point for a future enforce decision (it has an implementation body but no runtime composes it yet), and enforcing it is deliberately a separate decision, not this retirement. Why D3 semantic and not a D2 conversion: the chain walks a normalized STACK and `applyConversionsToStoredItem` maps a metadata type onto one of its collections. A package manifest is neither — `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a stack collection member and a stored manifest row passes that seam through unchanged. A conversion would be a transform with no seam that ever runs." }, { "surface": "kernel.dynamicLoadRequest / kernel.dynamicUnloadRequest / kernel.dynamicPluginResult / kernel.pluginSource / kernel.dynamicPluginOperation", "replacement": "(removed — there is no replacement shape, because there is no operation to describe. Plugins are composed at boot: `defineStack` registers them and the kernel runs register → init → start; the set is fixed until the process restarts. Delete the import and the value. Runtime plugin loading, if it is ever built, returns via the enforce route of ADR-0049 through a new ADR — loader first, vocabulary second)", "migrationId": "plugin-runtime-family-retired", "toMajor": 17, - "rationale": "The five schemas declared the \"Dynamic Loading\" capability — runtime load / unload / reload of plugins without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy — and NOTHING implemented it. A bare-name scan of objectstack, cloud and objectui found zero references outside this package's own declaration, its unit tests and the generated artifacts: no runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the published IDE bundle as proof the platform hot-loads plugins and constructs a request that parses clean and is received by nobody (#3950: an exported schema with no consumer is read as a capability). The #3896 follow-up removed this module's discovery/sandbox config island and left these five in place explicitly — \"operation contracts, not security promises; the enforce-or-remove call on them is a design decision rather than a correction\" — but that suspension lived only in a changeset paragraph with no issue carrying it. #4834 is that decision, answered REMOVE. `experimental` was considered and rejected: it is only `.describe()` prose and cannot stop an import, the weakest of the three ADR-0049 channels. None of the five is stored metadata — they are root request/result payload shapes embedded in no parent schema and parsed against no metadata document — so no `sys_metadata` row can carry one and there is no source for the D2 chain to rewrite; this entry is the D3 record. The removal also subsumes the kernel half of `plugin-activation-events-retired` (#4657): that tombstone goes with the shape that carried it. ADR-0049, #4834." + "rationale": "The five schemas declared the \"Dynamic Loading\" capability — runtime load / unload / reload of plugins without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy — and NOTHING implemented it. A bare-name scan of objectstack, cloud and objectui found zero references outside this package's own declaration, its unit tests and the generated artifacts: no runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the published IDE bundle as proof the platform hot-loads plugins and constructs a request that parses clean and is received by nobody (an exported schema with no consumer is read as a capability). The earlier removal of this module's discovery/sandbox config island — plugin sandboxing, integrity and approval settings that nothing read — left these five in place explicitly: \"operation contracts, not security promises; the enforce-or-remove call on them is a design decision rather than a correction\" — but that suspension lived only in a changeset paragraph with no issue carrying it. The maintainer's ruling of 2026-08-03 is that decision, answered REMOVE: hot loading is a real future capability, but nothing is being built and nothing pulls it, and when it is built its vocabulary enters the schema with the implementation. `experimental` was considered and rejected: it is only `.describe()` prose and cannot stop an import, the weakest of the three ADR-0049 channels. None of the five is stored metadata — they are root request/result payload shapes embedded in no parent schema and parsed against no metadata document — so no `sys_metadata` row can carry one and there is no source for the D2 chain to rewrite; this entry is the D3 record. The removal also subsumes the kernel half of `plugin-activation-events-retired`: that tombstone goes with the shape that carried it. ADR-0049." }, { "surface": "sys_position.permissions — the \"JSON-serialized array of permission strings\" textarea column left the platform position table declared by plugin-security (packages/plugins/plugin-security/src/objects/sys-position.object.ts), together with the clone_position copy entry that carried it between rows", @@ -844,24 +844,24 @@ }, { "surface": "ui.touchInteraction / ui.gestureConfig / ui.dndConfig / ui.keyboardNavigationConfig / ui.componentAnimation / ui.motionConfig / ui.pageTransition / ui.offlineConfig (the whole export surface of ui/touch.zod.ts, ui/dnd.zod.ts, ui/keyboard.zod.ts, ui/animation.zod.ts and ui/offline.zod.ts — 32 defs, 64 exported names)", - "replacement": "(removed — there is no replacement key, because there was never a key. Touch targets, drag-and-drop, focus management, keyboard shortcuts and motion are RENDERER BUILT-IN behaviour: the component library decides them, not a per-page metadata author. Offline is a platform capability, and its vocabulary belongs on the sync engine that owns the queue, the conflict policy and the cache — none of which exists yet. Delete the import and the value. Whichever of these earns real product pull returns WITH its own vocabulary and its executor, the #4910 way, not by un-retiring a declaration)", + "replacement": "(removed — there is no replacement key, because there was never a key. Touch targets, drag-and-drop, focus management, keyboard shortcuts and motion are RENDERER BUILT-IN behaviour: the component library decides them, not a per-page metadata author. Offline is a platform capability, and its vocabulary belongs on the sync engine that owns the queue, the conflict policy and the cache — none of which exists yet. Delete the import and the value. Whichever of these earns real product pull returns WITH its own vocabulary and its executor — the way inbound rate limiting came back, as a new key carrying only what its executor consumes — not by un-retiring a declaration)", "migrationId": "ui-interaction-config-family-retired", "toMajor": 17, - "rationale": "Five `@objectstack/spec/ui` modules declared a full interaction-configuration vocabulary — 22 `z.object` sites across touch/gesture, drag-and-drop, focus/keyboard, animation/motion and offline/sync — and NOTHING in the protocol carried them. This is the ADR-0049 false-compliance shape in its most inviting form for an AI author (ADR-0033), and worse than the ordinary declared-but-unread defect: `authorable-surface.json` listed 109 keys under these defs and `content/docs/references/ui/{touch,dnd,keyboard,animation,offline}.mdx` rendered them as authoring tables, so the published documentation advertised a vocabulary with no carrier key anywhere. An author following `dnd.mdx` and writing a `dnd:` block onto a page component was rejected by `PageComponentSchema` for an unrecognized key — the docs and the schema disagreeing about the platform (Prime Directive #10). Three independent measurements, each with its controls passing in the same run: (1) no module under `packages/spec/src` imported any of the five except the `ui/index.ts` barrel, so no schema declared a carrier key; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema` (25 roots, 4742 nodes) reached none of the 21 named object shapes, while `PageSchema`, `WebhookSchema` and `StateMachineSchema` all resolved `direct` and a synthetic carrier flipped all 21 — so unreachability was a fact about the graph, not a broken walker; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud outside these modules' own unit tests. objectui holds TYPE re-exports and parity ratchets, never validators, and says so (#2561). The 2026-08-04 ruling weighed wiring a carrier key (option B) and rejected it: that is a feature with a renderer behind it, not ledger clean-up. It also weighed tightening the shapes to `strictObject` and rejected that explicitly — strictness is a property of a PARSE and there is no parse, so it would spend a breaking change to leave \"a precisely validated dead slot, the more convincing lie\" (#4583). Because there was no carrier key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 conversion to rewrite: this entry is the D3 record, the same route 3 as #4834 (kernel plugin-runtime family) and #4938 (`HttpServerConfig`). ⚠️ Not to be confused with #5021, which retired the THEME `animation` block — a different file, different defs, and that one did have a carrier key and therefore a tombstone. ADR-0049, #4988." + "rationale": "Five `@objectstack/spec/ui` modules declared a full interaction-configuration vocabulary — 22 `z.object` sites across touch/gesture, drag-and-drop, focus/keyboard, animation/motion and offline/sync — and NOTHING in the protocol carried them. This is the ADR-0049 false-compliance shape in its most inviting form for an AI author (ADR-0033), and worse than the ordinary declared-but-unread defect: `authorable-surface.json` listed 109 keys under these defs and `content/docs/references/ui/{touch,dnd,keyboard,animation,offline}.mdx` rendered them as authoring tables, so the published documentation advertised a vocabulary with no carrier key anywhere. An author following `dnd.mdx` and writing a `dnd:` block onto a page component was rejected by `PageComponentSchema` for an unrecognized key — the docs and the schema disagreeing about the platform (Prime Directive #10). Three independent measurements, each with its controls passing in the same run: (1) no module under `packages/spec/src` imported any of the five except the `ui/index.ts` barrel, so no schema declared a carrier key; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema` (25 roots, 4742 nodes) reached none of the 21 named object shapes, while `PageSchema`, `WebhookSchema` and `StateMachineSchema` all resolved `direct` and a synthetic carrier flipped all 21 — so unreachability was a fact about the graph, not a broken walker; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud outside these modules' own unit tests. objectui holds TYPE re-exports and parity ratchets, never validators, and says so (its types package deliberately dropped the spec/ui zod-validator re-exports and keeps type-only ones). The 2026-08-04 ruling retired the family — touch, drag-and-drop, keyboard and motion are renderer built-in behaviour and offline belongs to a sync engine, none of it per-page metadata — and weighed wiring a carrier key (option B) and rejected it: that is a feature with a renderer behind it, not ledger clean-up. It also weighed tightening the shapes to `strictObject` and rejected that explicitly — strictness is a property of a PARSE and there is no parse, so it would spend a breaking change to leave \"a precisely validated dead slot, the more convincing lie\" (the lesson of the datasource capability flags: `readOnly` was precisely validated and read by nothing, while a shipped example called a datasource a read replica and wrote through it). Because there was no carrier key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 conversion to rewrite: this entry is the D3 record, the same route 3 as `plugin-runtime-family-retired` (the kernel plugin-runtime family) and the `HttpServerConfig` retirement (seven keys no runtime read and no authoring door reached, retired with their container). ⚠️ Not to be confused with the theme-token retirement (theme-driven typography is not a near-term capability, so nine token groups nothing consumed were retired), which retired the THEME `animation` block — a different file, different defs, and that one did have a carrier key and therefore a tombstone. ADR-0049." }, { "surface": "ui.notificationAction / ui.embedConfig", "replacement": "(removed — there is no replacement shape, because there was never a key to write either into. Delete the import and the value. Notification presentation is still described by the surviving `NotificationType` / `NotificationSeverity` / `NotificationPosition` vocabulary; public access to a form is granted by the LIVE `FormView.sharing` block (`SharingConfig`), which is untouched. Notification action buttons as metadata, and iframe embedding, return via the enforce route of ADR-0049 through a new ADR — carrier key and renderer first, vocabulary second)", "migrationId": "ui-notification-action-embed-config-retired", "toMajor": 17, - "rationale": "Both shapes were published `@objectstack/spec/ui` vocabulary with NO AUTHORING DOOR. #4001 批 14 measured them three ways on 2026-08-03 and this retirement re-ran all three against `origin/main` before removing anything, each with a positive control that passed in the same run: (1) CARRIER — no schema in `packages/spec/src` declared a key of either type (`ui/notification.zod`'s only non-test importer was the barrel; `ui/sharing.zod`'s were the barrel and `ui/view.zod.ts`, which names its SIBLING `SharingConfigSchema`), measured by resolving specifiers rather than substring-matching, because the repo holds two `sharing.zod` modules and a substring test miscredits `stack.zod.ts` to the UI one; (2) REACHABILITY — a BFS from the 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema`, over `build-schemas.ts`'s own walk including its derived-clone bridge, never reached either, while `Page` / `Action` / `DashboardWidget` / `Webhook` and `SharingConfig` itself all resolved `root-graph` in the same run and an injected synthetic carrier flipped both; (3) PARSE — zero `.parse()` in objectstack, cloud or objectui outside their own unit tests. So nobody could author one and nothing ever validated one: the #3950 shape, an exported schema with no consumer read as a capability, and the ADR-0033 trap where an AI author takes `EmbedConfigSchema` in the published bundle as proof the platform serves iframes. Neither is stored metadata and neither has a carrier, so no `sys_metadata` row can hold one and there is no source for the D2 chain to rewrite; this entry is the D3 record. 批 14 deliberately did NOT close them with `.strict()` — strictness is a property of a PARSE, and closing a shape nothing parses buys only \"a precisely-validated dead slot, the more convincing lie\" (#4583) — and filed the disposition as #5015, ruled REMOVE on 2026-08-04. Each was orphaned by an earlier retirement one level up: `NotificationAction` lost its wrappers at #4610 (`NotificationSchema` / `NotificationConfigSchema`, the #4535 C3 dual-source cleanup — that retirement's published \"zero consumers\" evidence was later falsified for objectui and is corrected on `ui/notification.zod`'s tombstone; the removal itself stands, #5781), and `EmbedConfig` lost its key at 17.0.0 when the 2026-06 liveness audit retired `App.embed` (no iframe route ever read it) — that key still stands as a `retiredKey()` tombstone in `app.zod.ts`, so an author who wrote the KEY already meets a prescription; this removes the value shape that outlived it. ⚠️ The retirement is per SCHEMA, not per file: `ui/sharing.zod` KEEPS `SharingConfigSchema`, a live door carried by `FormViewSchema.sharing` and read by `rest-server.ts` to mount the anonymous form routes, and `ui/notification.zod` keeps its three presentation enums. objectui consumed `NotificationActionSchema.shape.variant` as a VOCABULARY (never a parse) to pin its own hand-written `NotificationActionButton` interface — which is exactly why \"has a consumer\" never meant \"has an authoring door\" here; that pin is adapted objectui-side when it refreshes this dependency. ADR-0049, #5015." + "rationale": "Both shapes were published `@objectstack/spec/ui` vocabulary with NO AUTHORING DOOR. The v17 unknown-key strictness sweep (its ui/ batch 14), which measured each ui/ file for an authoring door before closing any shape, measured them three ways on 2026-08-03 and this retirement re-ran all three against `origin/main` before removing anything, each with a positive control that passed in the same run: (1) CARRIER — no schema in `packages/spec/src` declared a key of either type (`ui/notification.zod`'s only non-test importer was the barrel; `ui/sharing.zod`'s were the barrel and `ui/view.zod.ts`, which names its SIBLING `SharingConfigSchema`), measured by resolving specifiers rather than substring-matching, because the repo holds two `sharing.zod` modules and a substring test miscredits `stack.zod.ts` to the UI one; (2) REACHABILITY — a BFS from the 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema`, over `build-schemas.ts`'s own walk including its derived-clone bridge, never reached either, while `Page` / `Action` / `DashboardWidget` / `Webhook` and `SharingConfig` itself all resolved `root-graph` in the same run and an injected synthetic carrier flipped both; (3) PARSE — zero `.parse()` in objectstack, cloud or objectui outside their own unit tests. So nobody could author one and nothing ever validated one: the shape of the unwired plugin sandboxing config removed before it, an exported schema with no consumer read as a capability, and the ADR-0033 trap where an AI author takes `EmbedConfigSchema` in the published bundle as proof the platform serves iframes. Neither is stored metadata and neither has a carrier, so no `sys_metadata` row can hold one and there is no source for the D2 chain to rewrite; this entry is the D3 record. That batch deliberately did NOT close them with `.strict()` — strictness is a property of a PARSE, and closing a shape nothing parses buys only \"a precisely-validated dead slot, the more convincing lie\" (the lesson of the datasource capability flags, whose `readOnly` was precisely validated and read by nothing) — and left the disposition to ADR-0049's enforce-or-remove, which came back REMOVE on 2026-08-04: a dead surface with no authoring door retires implementation-first, as three same-shape rulings that week had already decided. Each was orphaned by an earlier retirement one level up: `NotificationAction` lost its wrappers when the dual-source cleanup removed the `./ui` copies of `NotificationSchema` / `NotificationConfigSchema` (the same names declared differently on other entry points) — that retirement's published \"zero consumers\" evidence was later falsified for objectui, which re-exported both names, and is corrected on `ui/notification.zod`'s tombstone; the removal itself stands — and `EmbedConfig` lost its key at 17.0.0 when the 2026-06 liveness audit retired `App.embed` (no iframe route ever read it) — that key still stands as a `retiredKey()` tombstone in `app.zod.ts`, so an author who wrote the KEY already meets a prescription; this removes the value shape that outlived it. ⚠️ The retirement is per SCHEMA, not per file: `ui/sharing.zod` KEEPS `SharingConfigSchema`, a live door carried by `FormViewSchema.sharing` and read by `rest-server.ts` to mount the anonymous form routes, and `ui/notification.zod` keeps its three presentation enums. objectui consumed `NotificationActionSchema.shape.variant` as a VOCABULARY (never a parse) to pin its own hand-written `NotificationActionButton` interface — which is exactly why \"has a consumer\" never meant \"has an authoring door\" here; that pin is adapted objectui-side when it refreshes this dependency. ADR-0049." }, { "surface": "ui.widgetManifest / ui.widgetLifecycle / ui.widgetEvent / ui.widgetProperty / ui.widgetSource / ui.i18nObject / ui.pluralRule / ui.numberFormat / ui.dateFormat / ui.localeConfig (the widget-registration vocabulary of ui/widget.zod.ts, and the five doorless shapes of ui/i18n.zod.ts — 10 defs, 26 exported names)", - "replacement": "(removed — there is no replacement key, because there was never a key. A custom field widget is still named the same way it always was: `field.widget` is a plain string naming a component the RENDERER has registered, and objectui's registry has always carried its own runtime manifest for that (`RuntimeWidgetManifest` / `RuntimeWidgetSource` in `@object-ui/types`, objectui#3161 / #4115), which models different keys and never derived from these. For localisation: write the default-language string on `label` / `description` — the framework generates the translation key at registration time from the naming convention — and put translations in translation files, which is the LIVE `system/translation.zod.ts` surface. Widget registration and locale formatting as authorable protocol metadata return via the ENFORCE route of ADR-0049 through a new ADR — the registry / loader / formatter first, the vocabulary second)", + "replacement": "(removed — there is no replacement key, because there was never a key. A custom field widget is still named the same way it always was: `field.widget` is a plain string naming a component the RENDERER has registered, and objectui's registry has always carried its own runtime manifest for that (`RuntimeWidgetManifest` / `RuntimeWidgetSource` in `@object-ui/types`, renamed off the spec's names under objectui's rule that a symbol named like a spec export must import it or take a name of its own), which models different keys and never derived from these. For localisation: write the default-language string on `label` / `description` — the framework generates the translation key at registration time from the naming convention — and put translations in translation files, which is the LIVE `system/translation.zod.ts` surface. Widget registration and locale formatting as authorable protocol metadata return via the ENFORCE route of ADR-0049 through a new ADR — the registry / loader / formatter first, the vocabulary second)", "migrationId": "ui-widget-i18n-family-retired", "toMajor": 17, - "rationale": "`ui/widget.zod.ts` published a complete widget-registration vocabulary — a manifest with lifecycle hooks, custom events, configurable properties and an npm/remote/inline implementation-source union — and `ui/i18n.zod.ts` published a structured-label, plural-rule and locale-formatting vocabulary. NOTHING in the protocol carried either. Three independent measurements, re-run on `origin/main` immediately before the removal with their controls passing in the SAME run: (1) no module under `packages/spec/src` imported `widget.zod` at all, and the only imports of `i18n.zod` anywhere name `I18nLabelSchema` / `AriaPropsSchema` (both KEPT), so no schema declared a carrier key — `field.widget` is a `z.string()` naming a registered component and has never referenced `WidgetManifest`; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema` reached none of them, while `PageSchema` / `ObjectListViewSchema` resolved `direct` in the same run and a synthetic carrier flipped every one of them; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud outside these files' own unit tests. `NumberFormat` / `DateFormat` DID have a carrier key (`LocaleConfig.numberFormat` / `.dateFormat`) but the carrier was itself doorless, so the subtree was `no door` rather than `no gate` and goes whole — leaving the two leaves behind would strand exported schemas with no consumer (#3950). `I18nObjectSchema` was additionally superseded by its own file-neighbour: `I18nLabelSchema`'s documentation already says translation keys are generated at registration time and translations live in translation files, and the live translation surface is `system/translation.zod.ts`, which uses none of these shapes. The 2026-08-06 ruling weighed giving them a carrier (option B) and rejected it: that is a feature with a registry and a renderer behind it, not ledger clean-up. Tightening them to `strictObject` was rejected earlier and explicitly (#4001 批 16) — strictness is a property of a PARSE and there is no parse, so it would spend a breaking change to leave \"a precisely validated dead slot, the more convincing lie\" (#4583). With no carrier key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 conversion to rewrite: this entry is the D3 record, route 3, the same shape as #4988 (the ui/ interaction config family), #4834 (kernel plugin-runtime family) and #4938 (`HttpServerConfig`). ⚠️ `WidgetManifest.performance`'s own `retiredKey()` tombstone (#3896 close-out) is SUBSUMED here, the #4657/#4834 way: it goes with the shape that carried it, which is strictly stronger than the tombstone, because there is no longer a manifest to author the key INTO. ⚠️ One of the nine widget sites is deliberately NOT retired. `FieldWidgetPropsSchema` survives: it is a REACT PROPS CONTRACT rather than authorable metadata (it never appeared in `authorable-surface/` or `json-schema.manifest/` — its `onChange` is a `z.function()`), so \"zero parse\" is its design and not its defect, and it acquired a live cross-repo compile-time consumer one day before 批 16 measured: objectui PR #3289 (2026-08-03) renamed `@object-ui/fields`' validation slot onto the spec's `error` with no alias, the form renderer began producing it, and `packages/fields/src/__tests__/spec-symbol-batch7.test.ts` pins the shape against `import type { FieldWidgetProps } from '@objectstack/spec/ui'` as an intentional tripwire. Re-verified on objectui `origin/main` 2026-08-07. ADR-0049, #5055." + "rationale": "`ui/widget.zod.ts` published a complete widget-registration vocabulary — a manifest with lifecycle hooks, custom events, configurable properties and an npm/remote/inline implementation-source union — and `ui/i18n.zod.ts` published a structured-label, plural-rule and locale-formatting vocabulary. NOTHING in the protocol carried either. Three independent measurements, re-run on `origin/main` immediately before the removal with their controls passing in the SAME run: (1) no module under `packages/spec/src` imported `widget.zod` at all, and the only imports of `i18n.zod` anywhere name `I18nLabelSchema` / `AriaPropsSchema` (both KEPT), so no schema declared a carrier key — `field.widget` is a `z.string()` naming a registered component and has never referenced `WidgetManifest`; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema` reached none of them, while `PageSchema` / `ObjectListViewSchema` resolved `direct` in the same run and a synthetic carrier flipped every one of them; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud outside these files' own unit tests. `NumberFormat` / `DateFormat` DID have a carrier key (`LocaleConfig.numberFormat` / `.dateFormat`) but the carrier was itself doorless, so the subtree was `no door` rather than `no gate` and goes whole — leaving the two leaves behind would strand exported schemas with no consumer, which an author reads as a capability. `I18nObjectSchema` was additionally superseded by its own file-neighbour: `I18nLabelSchema`'s documentation already says translation keys are generated at registration time and translations live in translation files, and the live translation surface is `system/translation.zod.ts`, which uses none of these shapes. The 2026-08-06 ruling weighed giving them a carrier (option B) and rejected it: that is a feature with a registry and a renderer behind it, not ledger clean-up. Tightening them to `strictObject` was rejected earlier and explicitly, by the batch of the v17 unknown-key strictness sweep that measured this file as having no authoring door — strictness is a property of a PARSE and there is no parse, so it would spend a breaking change to leave \"a precisely validated dead slot, the more convincing lie\" (the lesson of the datasource capability flags, whose `readOnly` was precisely validated and read by nothing). With no carrier key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 conversion to rewrite: this entry is the D3 record, route 3, the same shape as `ui-interaction-config-family-retired`, `plugin-runtime-family-retired` and the `HttpServerConfig` retirement. ⚠️ `WidgetManifest.performance`'s own `retiredKey()` tombstone (left by the close-out sweep that removed the inert `performance` keys no renderer applied) is SUBSUMED here, the way the kernel `activationEvents` tombstone went with the removed plugin-runtime family: it goes with the shape that carried it, which is strictly stronger than the tombstone, because there is no longer a manifest to author the key INTO. ⚠️ One of the nine widget sites is deliberately NOT retired. `FieldWidgetPropsSchema` survives: it is a REACT PROPS CONTRACT rather than authorable metadata (it never appeared in `authorable-surface/` or `json-schema.manifest/` — its `onChange` is a `z.function()`), so \"zero parse\" is its design and not its defect, and it acquired a live cross-repo compile-time consumer one day before that sweep batch measured: an objectui fix of 2026-08-03, made to follow the spec, renamed `@object-ui/fields`' validation slot onto the spec's `error` with no alias, the form renderer began producing it, and `packages/fields/src/__tests__/spec-symbol-batch7.test.ts` pins the shape against `import type { FieldWidgetProps } from '@objectstack/spec/ui'` as an intentional tripwire. Re-verified on objectui `origin/main` 2026-08-07. ADR-0049." }, { "surface": "sys_user_permission_set.delegated_from — the ADR-0091 D3 provenance column left the platform grant table declared by plugin-security (packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts). The sibling declaration on sys_user_position is untouched", @@ -1606,21 +1606,21 @@ "replacement": "(removed — delete the key. Every plugin activates immediately on load/registration, which is the only behaviour that has ever existed; `activate()` still runs at registration time. Lazy activation, if built, returns via the enforce route of ADR-0049 through a new ADR, with a vocabulary its executor actually honours)", "migrationId": "plugin-activation-events-retired", "toMajor": 17, - "rationale": "Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary they embedded (`onCommand` / `onRoute` / … / `onView` after the #4653 convergence) — promised lazy plugin activation (\"plugins remain dormant until an activation event fires\") that no runtime in objectstack, cloud, cloud-v1 or objectui ever implemented: nothing anywhere read the key, every plugin activates immediately, and cloud-v1's own ROADMAP recorded lazy activation as unimplemented (planned v0.4.0). That is the ADR-0049 false-compliance shape in the semantically-lying direction: an author writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` expected deferral and got eager activation with a clean parse. Neither parent shape is stored metadata — `StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin` (a root schema, never part of a stack tree) and `DynamicLoadRequest` is a runtime request shape with no caller — so no `sys_metadata` row can carry the key and there is no source for the D2 chain to rewrite; this entry is the D3 record. The kernel key is tombstoned via `retiredKey()` (its schema is not `.strict()`; a plain delete would strip an authored value silently), the studio key is rejected by the strict manifest parse with a guidance prescription (as are its former VS Code-flavoured aliases `activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / `ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys (#3950: an exported schema with no consumer is read as a capability). #4657. SUPERSEDED ON THE KERNEL SIDE by #4834 (same unreleased major): the whole `DynamicLoadRequest` shape — and the rest of the plugin-runtime family with it — was removed, which took this key's `retiredKey()` tombstone with it. That is strictly stronger than the tombstone, not weaker: there is no longer a `DynamicLoadRequest` to author the key INTO, so the prescription an author needs is no longer \"delete this key\" but \"this request shape does not exist\" (see `plugin-runtime-family-retired`). The studio half of this entry is unaffected and still enforced by the strict manifest parse." + "rationale": "Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary they embedded (`onCommand` / `onRoute` / … / `onView`, once the kernel and studio copies had converged on the kernel's structured `{ type, pattern }` shape) — promised lazy plugin activation (\"plugins remain dormant until an activation event fires\") that no runtime in objectstack, cloud, cloud-v1 or objectui ever implemented: nothing anywhere read the key, every plugin activates immediately, and cloud-v1's own ROADMAP recorded lazy activation as unimplemented (planned v0.4.0). That is the ADR-0049 false-compliance shape in the semantically-lying direction: an author writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` expected deferral and got eager activation with a clean parse. Neither parent shape is stored metadata — `StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin` (a root schema, never part of a stack tree) and `DynamicLoadRequest` is a runtime request shape with no caller — so no `sys_metadata` row can carry the key and there is no source for the D2 chain to rewrite; this entry is the D3 record. The kernel key is tombstoned via `retiredKey()` (its schema is not `.strict()`; a plain delete would strip an authored value silently), the studio key is rejected by the strict manifest parse with a guidance prescription (as are its former VS Code-flavoured aliases `activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / `ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys (the lesson of the unwired plugin sandboxing / integrity / approval config removed before this: an exported schema with no consumer is read as a capability). Both keys took ADR-0049's REMOVE answer, not ENFORCE, while protocol 17 was still unreleased. SUPERSEDED ON THE KERNEL SIDE by the maintainer's REMOVE ruling on the rest of the plugin-runtime family (same unreleased major): the whole `DynamicLoadRequest` shape — and the rest of the plugin-runtime family with it — was removed, which took this key's `retiredKey()` tombstone with it. That is strictly stronger than the tombstone, not weaker: there is no longer a `DynamicLoadRequest` to author the key INTO, so the prescription an author needs is no longer \"delete this key\" but \"this request shape does not exist\" (see `plugin-runtime-family-retired`). The studio half of this entry is unaffected and still enforced by the strict manifest parse." }, { "surface": "manifest.loading (the whole block: strategy / preload / codeSplitting / dynamicImport / initialization / dependencyResolution / hotReload / caching / sandboxing / monitoring)", "replacement": "nothing to re-declare — delete the key. Plugins are composed at boot: `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder` in `packages/core/src/plugin-order.ts`). For the isolation `loading.sandboxing` appeared to configure, note that the plugin trust tier (`manifest.runtime`, ADR-0025 §3.6) does not supply it either: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the manifest permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation", "migrationId": "plugin-manifest-loading-retired", "toMajor": 17, - "rationale": "ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a complete plugin loading policy and NOTHING read it. A bare-name scan of all three repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), each with a control probe proving the scan saw the tree — put every hit inside `packages/spec` itself: this module's own declaration, its own unit tests, the `Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key parsed, entered the manifest, and changed nothing — #3950, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform isolates plugins, wrote the config, and received a clean parse and zero isolation. An inert security control is worse than an absent one because it is believed. Hot reload was additionally a TWO-SOURCE defect: the docs pointed at this dead `PluginHotReloadSchema` while the only implementation body, `HotReloadManager` (`packages/core/src/hot-reload.ts`), reads a different vocabulary — `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`. Ruling §2 converges on the surviving side: that schema is KEPT as the starting point for a future enforce decision (it has an implementation body but no runtime composes it yet), and enforcing it is deliberately a separate decision, not this retirement. Why D3 semantic and not a D2 conversion: the chain walks a normalized STACK and `applyConversionsToStoredItem` maps a metadata type onto one of its collections. A package manifest is neither — `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a stack collection member and a stored manifest row passes that seam through unchanged. A conversion would be a transform with no seam that ever runs." + "rationale": "ADR-0049 enforce-or-remove; the maintainer ruled REMOVE on 2026-08-04, on condition that a bare-name sweep of cloud and objectui came back clean first. The block declared a complete plugin loading policy and NOTHING read it. A bare-name scan of all three repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), each with a control probe proving the scan saw the tree — put every hit inside `packages/spec` itself: this module's own declaration, its own unit tests, the `Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key parsed, entered the manifest, and changed nothing — an exported schema with no consumer, read as a capability, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform isolates plugins, wrote the config, and received a clean parse and zero isolation. An inert security control is worse than an absent one because it is believed. Hot reload was additionally a TWO-SOURCE defect: the docs pointed at this dead `PluginHotReloadSchema` while the only implementation body, `HotReloadManager` (`packages/core/src/hot-reload.ts`), reads a different vocabulary — `HotReloadConfigSchema` in `plugin-lifecycle-advanced.zod.ts`. Ruling §2 converges on the surviving side: that schema is KEPT as the starting point for a future enforce decision (it has an implementation body but no runtime composes it yet), and enforcing it is deliberately a separate decision, not this retirement. Why D3 semantic and not a D2 conversion: the chain walks a normalized STACK and `applyConversionsToStoredItem` maps a metadata type onto one of its collections. A package manifest is neither — `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry, so a manifest is not a stack collection member and a stored manifest row passes that seam through unchanged. A conversion would be a transform with no seam that ever runs." }, { "surface": "kernel.dynamicLoadRequest / kernel.dynamicUnloadRequest / kernel.dynamicPluginResult / kernel.pluginSource / kernel.dynamicPluginOperation", "replacement": "(removed — there is no replacement shape, because there is no operation to describe. Plugins are composed at boot: `defineStack` registers them and the kernel runs register → init → start; the set is fixed until the process restarts. Delete the import and the value. Runtime plugin loading, if it is ever built, returns via the enforce route of ADR-0049 through a new ADR — loader first, vocabulary second)", "migrationId": "plugin-runtime-family-retired", "toMajor": 17, - "rationale": "The five schemas declared the \"Dynamic Loading\" capability — runtime load / unload / reload of plugins without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy — and NOTHING implemented it. A bare-name scan of objectstack, cloud and objectui found zero references outside this package's own declaration, its unit tests and the generated artifacts: no runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the published IDE bundle as proof the platform hot-loads plugins and constructs a request that parses clean and is received by nobody (#3950: an exported schema with no consumer is read as a capability). The #3896 follow-up removed this module's discovery/sandbox config island and left these five in place explicitly — \"operation contracts, not security promises; the enforce-or-remove call on them is a design decision rather than a correction\" — but that suspension lived only in a changeset paragraph with no issue carrying it. #4834 is that decision, answered REMOVE. `experimental` was considered and rejected: it is only `.describe()` prose and cannot stop an import, the weakest of the three ADR-0049 channels. None of the five is stored metadata — they are root request/result payload shapes embedded in no parent schema and parsed against no metadata document — so no `sys_metadata` row can carry one and there is no source for the D2 chain to rewrite; this entry is the D3 record. The removal also subsumes the kernel half of `plugin-activation-events-retired` (#4657): that tombstone goes with the shape that carried it. ADR-0049, #4834." + "rationale": "The five schemas declared the \"Dynamic Loading\" capability — runtime load / unload / reload of plugins without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy — and NOTHING implemented it. A bare-name scan of objectstack, cloud and objectui found zero references outside this package's own declaration, its unit tests and the generated artifacts: no runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the published IDE bundle as proof the platform hot-loads plugins and constructs a request that parses clean and is received by nobody (an exported schema with no consumer is read as a capability). The earlier removal of this module's discovery/sandbox config island — plugin sandboxing, integrity and approval settings that nothing read — left these five in place explicitly: \"operation contracts, not security promises; the enforce-or-remove call on them is a design decision rather than a correction\" — but that suspension lived only in a changeset paragraph with no issue carrying it. The maintainer's ruling of 2026-08-03 is that decision, answered REMOVE: hot loading is a real future capability, but nothing is being built and nothing pulls it, and when it is built its vocabulary enters the schema with the implementation. `experimental` was considered and rejected: it is only `.describe()` prose and cannot stop an import, the weakest of the three ADR-0049 channels. None of the five is stored metadata — they are root request/result payload shapes embedded in no parent schema and parsed against no metadata document — so no `sys_metadata` row can carry one and there is no source for the D2 chain to rewrite; this entry is the D3 record. The removal also subsumes the kernel half of `plugin-activation-events-retired`: that tombstone goes with the shape that carried it. ADR-0049." }, { "surface": "sys_position.permissions — the \"JSON-serialized array of permission strings\" textarea column left the platform position table declared by plugin-security (packages/plugins/plugin-security/src/objects/sys-position.object.ts), together with the clone_position copy entry that carried it between rows", @@ -1736,24 +1736,24 @@ }, { "surface": "ui.touchInteraction / ui.gestureConfig / ui.dndConfig / ui.keyboardNavigationConfig / ui.componentAnimation / ui.motionConfig / ui.pageTransition / ui.offlineConfig (the whole export surface of ui/touch.zod.ts, ui/dnd.zod.ts, ui/keyboard.zod.ts, ui/animation.zod.ts and ui/offline.zod.ts — 32 defs, 64 exported names)", - "replacement": "(removed — there is no replacement key, because there was never a key. Touch targets, drag-and-drop, focus management, keyboard shortcuts and motion are RENDERER BUILT-IN behaviour: the component library decides them, not a per-page metadata author. Offline is a platform capability, and its vocabulary belongs on the sync engine that owns the queue, the conflict policy and the cache — none of which exists yet. Delete the import and the value. Whichever of these earns real product pull returns WITH its own vocabulary and its executor, the #4910 way, not by un-retiring a declaration)", + "replacement": "(removed — there is no replacement key, because there was never a key. Touch targets, drag-and-drop, focus management, keyboard shortcuts and motion are RENDERER BUILT-IN behaviour: the component library decides them, not a per-page metadata author. Offline is a platform capability, and its vocabulary belongs on the sync engine that owns the queue, the conflict policy and the cache — none of which exists yet. Delete the import and the value. Whichever of these earns real product pull returns WITH its own vocabulary and its executor — the way inbound rate limiting came back, as a new key carrying only what its executor consumes — not by un-retiring a declaration)", "migrationId": "ui-interaction-config-family-retired", "toMajor": 17, - "rationale": "Five `@objectstack/spec/ui` modules declared a full interaction-configuration vocabulary — 22 `z.object` sites across touch/gesture, drag-and-drop, focus/keyboard, animation/motion and offline/sync — and NOTHING in the protocol carried them. This is the ADR-0049 false-compliance shape in its most inviting form for an AI author (ADR-0033), and worse than the ordinary declared-but-unread defect: `authorable-surface.json` listed 109 keys under these defs and `content/docs/references/ui/{touch,dnd,keyboard,animation,offline}.mdx` rendered them as authoring tables, so the published documentation advertised a vocabulary with no carrier key anywhere. An author following `dnd.mdx` and writing a `dnd:` block onto a page component was rejected by `PageComponentSchema` for an unrecognized key — the docs and the schema disagreeing about the platform (Prime Directive #10). Three independent measurements, each with its controls passing in the same run: (1) no module under `packages/spec/src` imported any of the five except the `ui/index.ts` barrel, so no schema declared a carrier key; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema` (25 roots, 4742 nodes) reached none of the 21 named object shapes, while `PageSchema`, `WebhookSchema` and `StateMachineSchema` all resolved `direct` and a synthetic carrier flipped all 21 — so unreachability was a fact about the graph, not a broken walker; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud outside these modules' own unit tests. objectui holds TYPE re-exports and parity ratchets, never validators, and says so (#2561). The 2026-08-04 ruling weighed wiring a carrier key (option B) and rejected it: that is a feature with a renderer behind it, not ledger clean-up. It also weighed tightening the shapes to `strictObject` and rejected that explicitly — strictness is a property of a PARSE and there is no parse, so it would spend a breaking change to leave \"a precisely validated dead slot, the more convincing lie\" (#4583). Because there was no carrier key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 conversion to rewrite: this entry is the D3 record, the same route 3 as #4834 (kernel plugin-runtime family) and #4938 (`HttpServerConfig`). ⚠️ Not to be confused with #5021, which retired the THEME `animation` block — a different file, different defs, and that one did have a carrier key and therefore a tombstone. ADR-0049, #4988." + "rationale": "Five `@objectstack/spec/ui` modules declared a full interaction-configuration vocabulary — 22 `z.object` sites across touch/gesture, drag-and-drop, focus/keyboard, animation/motion and offline/sync — and NOTHING in the protocol carried them. This is the ADR-0049 false-compliance shape in its most inviting form for an AI author (ADR-0033), and worse than the ordinary declared-but-unread defect: `authorable-surface.json` listed 109 keys under these defs and `content/docs/references/ui/{touch,dnd,keyboard,animation,offline}.mdx` rendered them as authoring tables, so the published documentation advertised a vocabulary with no carrier key anywhere. An author following `dnd.mdx` and writing a `dnd:` block onto a page component was rejected by `PageComponentSchema` for an unrecognized key — the docs and the schema disagreeing about the platform (Prime Directive #10). Three independent measurements, each with its controls passing in the same run: (1) no module under `packages/spec/src` imported any of the five except the `ui/index.ts` barrel, so no schema declared a carrier key; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema` (25 roots, 4742 nodes) reached none of the 21 named object shapes, while `PageSchema`, `WebhookSchema` and `StateMachineSchema` all resolved `direct` and a synthetic carrier flipped all 21 — so unreachability was a fact about the graph, not a broken walker; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud outside these modules' own unit tests. objectui holds TYPE re-exports and parity ratchets, never validators, and says so (its types package deliberately dropped the spec/ui zod-validator re-exports and keeps type-only ones). The 2026-08-04 ruling retired the family — touch, drag-and-drop, keyboard and motion are renderer built-in behaviour and offline belongs to a sync engine, none of it per-page metadata — and weighed wiring a carrier key (option B) and rejected it: that is a feature with a renderer behind it, not ledger clean-up. It also weighed tightening the shapes to `strictObject` and rejected that explicitly — strictness is a property of a PARSE and there is no parse, so it would spend a breaking change to leave \"a precisely validated dead slot, the more convincing lie\" (the lesson of the datasource capability flags: `readOnly` was precisely validated and read by nothing, while a shipped example called a datasource a read replica and wrote through it). Because there was no carrier key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 conversion to rewrite: this entry is the D3 record, the same route 3 as `plugin-runtime-family-retired` (the kernel plugin-runtime family) and the `HttpServerConfig` retirement (seven keys no runtime read and no authoring door reached, retired with their container). ⚠️ Not to be confused with the theme-token retirement (theme-driven typography is not a near-term capability, so nine token groups nothing consumed were retired), which retired the THEME `animation` block — a different file, different defs, and that one did have a carrier key and therefore a tombstone. ADR-0049." }, { "surface": "ui.notificationAction / ui.embedConfig", "replacement": "(removed — there is no replacement shape, because there was never a key to write either into. Delete the import and the value. Notification presentation is still described by the surviving `NotificationType` / `NotificationSeverity` / `NotificationPosition` vocabulary; public access to a form is granted by the LIVE `FormView.sharing` block (`SharingConfig`), which is untouched. Notification action buttons as metadata, and iframe embedding, return via the enforce route of ADR-0049 through a new ADR — carrier key and renderer first, vocabulary second)", "migrationId": "ui-notification-action-embed-config-retired", "toMajor": 17, - "rationale": "Both shapes were published `@objectstack/spec/ui` vocabulary with NO AUTHORING DOOR. #4001 批 14 measured them three ways on 2026-08-03 and this retirement re-ran all three against `origin/main` before removing anything, each with a positive control that passed in the same run: (1) CARRIER — no schema in `packages/spec/src` declared a key of either type (`ui/notification.zod`'s only non-test importer was the barrel; `ui/sharing.zod`'s were the barrel and `ui/view.zod.ts`, which names its SIBLING `SharingConfigSchema`), measured by resolving specifiers rather than substring-matching, because the repo holds two `sharing.zod` modules and a substring test miscredits `stack.zod.ts` to the UI one; (2) REACHABILITY — a BFS from the 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema`, over `build-schemas.ts`'s own walk including its derived-clone bridge, never reached either, while `Page` / `Action` / `DashboardWidget` / `Webhook` and `SharingConfig` itself all resolved `root-graph` in the same run and an injected synthetic carrier flipped both; (3) PARSE — zero `.parse()` in objectstack, cloud or objectui outside their own unit tests. So nobody could author one and nothing ever validated one: the #3950 shape, an exported schema with no consumer read as a capability, and the ADR-0033 trap where an AI author takes `EmbedConfigSchema` in the published bundle as proof the platform serves iframes. Neither is stored metadata and neither has a carrier, so no `sys_metadata` row can hold one and there is no source for the D2 chain to rewrite; this entry is the D3 record. 批 14 deliberately did NOT close them with `.strict()` — strictness is a property of a PARSE, and closing a shape nothing parses buys only \"a precisely-validated dead slot, the more convincing lie\" (#4583) — and filed the disposition as #5015, ruled REMOVE on 2026-08-04. Each was orphaned by an earlier retirement one level up: `NotificationAction` lost its wrappers at #4610 (`NotificationSchema` / `NotificationConfigSchema`, the #4535 C3 dual-source cleanup — that retirement's published \"zero consumers\" evidence was later falsified for objectui and is corrected on `ui/notification.zod`'s tombstone; the removal itself stands, #5781), and `EmbedConfig` lost its key at 17.0.0 when the 2026-06 liveness audit retired `App.embed` (no iframe route ever read it) — that key still stands as a `retiredKey()` tombstone in `app.zod.ts`, so an author who wrote the KEY already meets a prescription; this removes the value shape that outlived it. ⚠️ The retirement is per SCHEMA, not per file: `ui/sharing.zod` KEEPS `SharingConfigSchema`, a live door carried by `FormViewSchema.sharing` and read by `rest-server.ts` to mount the anonymous form routes, and `ui/notification.zod` keeps its three presentation enums. objectui consumed `NotificationActionSchema.shape.variant` as a VOCABULARY (never a parse) to pin its own hand-written `NotificationActionButton` interface — which is exactly why \"has a consumer\" never meant \"has an authoring door\" here; that pin is adapted objectui-side when it refreshes this dependency. ADR-0049, #5015." + "rationale": "Both shapes were published `@objectstack/spec/ui` vocabulary with NO AUTHORING DOOR. The v17 unknown-key strictness sweep (its ui/ batch 14), which measured each ui/ file for an authoring door before closing any shape, measured them three ways on 2026-08-03 and this retirement re-ran all three against `origin/main` before removing anything, each with a positive control that passed in the same run: (1) CARRIER — no schema in `packages/spec/src` declared a key of either type (`ui/notification.zod`'s only non-test importer was the barrel; `ui/sharing.zod`'s were the barrel and `ui/view.zod.ts`, which names its SIBLING `SharingConfigSchema`), measured by resolving specifiers rather than substring-matching, because the repo holds two `sharing.zod` modules and a substring test miscredits `stack.zod.ts` to the UI one; (2) REACHABILITY — a BFS from the 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema`, over `build-schemas.ts`'s own walk including its derived-clone bridge, never reached either, while `Page` / `Action` / `DashboardWidget` / `Webhook` and `SharingConfig` itself all resolved `root-graph` in the same run and an injected synthetic carrier flipped both; (3) PARSE — zero `.parse()` in objectstack, cloud or objectui outside their own unit tests. So nobody could author one and nothing ever validated one: the shape of the unwired plugin sandboxing config removed before it, an exported schema with no consumer read as a capability, and the ADR-0033 trap where an AI author takes `EmbedConfigSchema` in the published bundle as proof the platform serves iframes. Neither is stored metadata and neither has a carrier, so no `sys_metadata` row can hold one and there is no source for the D2 chain to rewrite; this entry is the D3 record. That batch deliberately did NOT close them with `.strict()` — strictness is a property of a PARSE, and closing a shape nothing parses buys only \"a precisely-validated dead slot, the more convincing lie\" (the lesson of the datasource capability flags, whose `readOnly` was precisely validated and read by nothing) — and left the disposition to ADR-0049's enforce-or-remove, which came back REMOVE on 2026-08-04: a dead surface with no authoring door retires implementation-first, as three same-shape rulings that week had already decided. Each was orphaned by an earlier retirement one level up: `NotificationAction` lost its wrappers when the dual-source cleanup removed the `./ui` copies of `NotificationSchema` / `NotificationConfigSchema` (the same names declared differently on other entry points) — that retirement's published \"zero consumers\" evidence was later falsified for objectui, which re-exported both names, and is corrected on `ui/notification.zod`'s tombstone; the removal itself stands — and `EmbedConfig` lost its key at 17.0.0 when the 2026-06 liveness audit retired `App.embed` (no iframe route ever read it) — that key still stands as a `retiredKey()` tombstone in `app.zod.ts`, so an author who wrote the KEY already meets a prescription; this removes the value shape that outlived it. ⚠️ The retirement is per SCHEMA, not per file: `ui/sharing.zod` KEEPS `SharingConfigSchema`, a live door carried by `FormViewSchema.sharing` and read by `rest-server.ts` to mount the anonymous form routes, and `ui/notification.zod` keeps its three presentation enums. objectui consumed `NotificationActionSchema.shape.variant` as a VOCABULARY (never a parse) to pin its own hand-written `NotificationActionButton` interface — which is exactly why \"has a consumer\" never meant \"has an authoring door\" here; that pin is adapted objectui-side when it refreshes this dependency. ADR-0049." }, { "surface": "ui.widgetManifest / ui.widgetLifecycle / ui.widgetEvent / ui.widgetProperty / ui.widgetSource / ui.i18nObject / ui.pluralRule / ui.numberFormat / ui.dateFormat / ui.localeConfig (the widget-registration vocabulary of ui/widget.zod.ts, and the five doorless shapes of ui/i18n.zod.ts — 10 defs, 26 exported names)", - "replacement": "(removed — there is no replacement key, because there was never a key. A custom field widget is still named the same way it always was: `field.widget` is a plain string naming a component the RENDERER has registered, and objectui's registry has always carried its own runtime manifest for that (`RuntimeWidgetManifest` / `RuntimeWidgetSource` in `@object-ui/types`, objectui#3161 / #4115), which models different keys and never derived from these. For localisation: write the default-language string on `label` / `description` — the framework generates the translation key at registration time from the naming convention — and put translations in translation files, which is the LIVE `system/translation.zod.ts` surface. Widget registration and locale formatting as authorable protocol metadata return via the ENFORCE route of ADR-0049 through a new ADR — the registry / loader / formatter first, the vocabulary second)", + "replacement": "(removed — there is no replacement key, because there was never a key. A custom field widget is still named the same way it always was: `field.widget` is a plain string naming a component the RENDERER has registered, and objectui's registry has always carried its own runtime manifest for that (`RuntimeWidgetManifest` / `RuntimeWidgetSource` in `@object-ui/types`, renamed off the spec's names under objectui's rule that a symbol named like a spec export must import it or take a name of its own), which models different keys and never derived from these. For localisation: write the default-language string on `label` / `description` — the framework generates the translation key at registration time from the naming convention — and put translations in translation files, which is the LIVE `system/translation.zod.ts` surface. Widget registration and locale formatting as authorable protocol metadata return via the ENFORCE route of ADR-0049 through a new ADR — the registry / loader / formatter first, the vocabulary second)", "migrationId": "ui-widget-i18n-family-retired", "toMajor": 17, - "rationale": "`ui/widget.zod.ts` published a complete widget-registration vocabulary — a manifest with lifecycle hooks, custom events, configurable properties and an npm/remote/inline implementation-source union — and `ui/i18n.zod.ts` published a structured-label, plural-rule and locale-formatting vocabulary. NOTHING in the protocol carried either. Three independent measurements, re-run on `origin/main` immediately before the removal with their controls passing in the SAME run: (1) no module under `packages/spec/src` imported `widget.zod` at all, and the only imports of `i18n.zod` anywhere name `I18nLabelSchema` / `AriaPropsSchema` (both KEPT), so no schema declared a carrier key — `field.widget` is a `z.string()` naming a registered component and has never referenced `WidgetManifest`; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema` reached none of them, while `PageSchema` / `ObjectListViewSchema` resolved `direct` in the same run and a synthetic carrier flipped every one of them; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud outside these files' own unit tests. `NumberFormat` / `DateFormat` DID have a carrier key (`LocaleConfig.numberFormat` / `.dateFormat`) but the carrier was itself doorless, so the subtree was `no door` rather than `no gate` and goes whole — leaving the two leaves behind would strand exported schemas with no consumer (#3950). `I18nObjectSchema` was additionally superseded by its own file-neighbour: `I18nLabelSchema`'s documentation already says translation keys are generated at registration time and translations live in translation files, and the live translation surface is `system/translation.zod.ts`, which uses none of these shapes. The 2026-08-06 ruling weighed giving them a carrier (option B) and rejected it: that is a feature with a registry and a renderer behind it, not ledger clean-up. Tightening them to `strictObject` was rejected earlier and explicitly (#4001 批 16) — strictness is a property of a PARSE and there is no parse, so it would spend a breaking change to leave \"a precisely validated dead slot, the more convincing lie\" (#4583). With no carrier key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 conversion to rewrite: this entry is the D3 record, route 3, the same shape as #4988 (the ui/ interaction config family), #4834 (kernel plugin-runtime family) and #4938 (`HttpServerConfig`). ⚠️ `WidgetManifest.performance`'s own `retiredKey()` tombstone (#3896 close-out) is SUBSUMED here, the #4657/#4834 way: it goes with the shape that carried it, which is strictly stronger than the tombstone, because there is no longer a manifest to author the key INTO. ⚠️ One of the nine widget sites is deliberately NOT retired. `FieldWidgetPropsSchema` survives: it is a REACT PROPS CONTRACT rather than authorable metadata (it never appeared in `authorable-surface/` or `json-schema.manifest/` — its `onChange` is a `z.function()`), so \"zero parse\" is its design and not its defect, and it acquired a live cross-repo compile-time consumer one day before 批 16 measured: objectui PR #3289 (2026-08-03) renamed `@object-ui/fields`' validation slot onto the spec's `error` with no alias, the form renderer began producing it, and `packages/fields/src/__tests__/spec-symbol-batch7.test.ts` pins the shape against `import type { FieldWidgetProps } from '@objectstack/spec/ui'` as an intentional tripwire. Re-verified on objectui `origin/main` 2026-08-07. ADR-0049, #5055." + "rationale": "`ui/widget.zod.ts` published a complete widget-registration vocabulary — a manifest with lifecycle hooks, custom events, configurable properties and an npm/remote/inline implementation-source union — and `ui/i18n.zod.ts` published a structured-label, plural-rule and locale-formatting vocabulary. NOTHING in the protocol carried either. Three independent measurements, re-run on `origin/main` immediately before the removal with their controls passing in the SAME run: (1) no module under `packages/spec/src` imported `widget.zod` at all, and the only imports of `i18n.zod` anywhere name `I18nLabelSchema` / `AriaPropsSchema` (both KEPT), so no schema declared a carrier key — `field.widget` is a `z.string()` naming a registered component and has never referenced `WidgetManifest`; (2) a BFS over the in-memory Zod graph from all 24 metadata-type roots plus `defineStack`'s `ObjectStackSchema` reached none of them, while `PageSchema` / `ObjectListViewSchema` resolved `direct` in the same run and a synthetic carrier flipped every one of them; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud outside these files' own unit tests. `NumberFormat` / `DateFormat` DID have a carrier key (`LocaleConfig.numberFormat` / `.dateFormat`) but the carrier was itself doorless, so the subtree was `no door` rather than `no gate` and goes whole — leaving the two leaves behind would strand exported schemas with no consumer, which an author reads as a capability. `I18nObjectSchema` was additionally superseded by its own file-neighbour: `I18nLabelSchema`'s documentation already says translation keys are generated at registration time and translations live in translation files, and the live translation surface is `system/translation.zod.ts`, which uses none of these shapes. The 2026-08-06 ruling weighed giving them a carrier (option B) and rejected it: that is a feature with a registry and a renderer behind it, not ledger clean-up. Tightening them to `strictObject` was rejected earlier and explicitly, by the batch of the v17 unknown-key strictness sweep that measured this file as having no authoring door — strictness is a property of a PARSE and there is no parse, so it would spend a breaking change to leave \"a precisely validated dead slot, the more convincing lie\" (the lesson of the datasource capability flags, whose `readOnly` was precisely validated and read by nothing). With no carrier key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 conversion to rewrite: this entry is the D3 record, route 3, the same shape as `ui-interaction-config-family-retired`, `plugin-runtime-family-retired` and the `HttpServerConfig` retirement. ⚠️ `WidgetManifest.performance`'s own `retiredKey()` tombstone (left by the close-out sweep that removed the inert `performance` keys no renderer applied) is SUBSUMED here, the way the kernel `activationEvents` tombstone went with the removed plugin-runtime family: it goes with the shape that carried it, which is strictly stronger than the tombstone, because there is no longer a manifest to author the key INTO. ⚠️ One of the nine widget sites is deliberately NOT retired. `FieldWidgetPropsSchema` survives: it is a REACT PROPS CONTRACT rather than authorable metadata (it never appeared in `authorable-surface/` or `json-schema.manifest/` — its `onChange` is a `z.function()`), so \"zero parse\" is its design and not its defect, and it acquired a live cross-repo compile-time consumer one day before that sweep batch measured: an objectui fix of 2026-08-03, made to follow the spec, renamed `@object-ui/fields`' validation slot onto the spec's `error` with no alias, the form renderer began producing it, and `packages/fields/src/__tests__/spec-symbol-batch7.test.ts` pins the shape against `import type { FieldWidgetProps } from '@objectstack/spec/ui'` as an intentional tripwire. Re-verified on objectui `origin/main` 2026-08-07. ADR-0049." }, { "surface": "sys_user_permission_set.delegated_from — the ADR-0091 D3 provenance column left the platform grant table declared by plugin-security (packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts). The sibling declaration on sys_user_position is untouched", diff --git a/packages/spec/src/migrations/entries/semantic/17.plugin-activation-events-retired.ts b/packages/spec/src/migrations/entries/semantic/17.plugin-activation-events-retired.ts index f9984761fcb..efc4ec40690 100644 --- a/packages/spec/src/migrations/entries/semantic/17.plugin-activation-events-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/17.plugin-activation-events-retired.ts @@ -13,7 +13,8 @@ export const entry: SemanticMigration = { + 'ADR-0049 through a new ADR, with a vocabulary its executor actually honours)', reason: 'Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary ' - + 'they embedded (`onCommand` / `onRoute` / … / `onView` after the #4653 convergence) — ' + + 'they embedded (`onCommand` / `onRoute` / … / `onView`, once the kernel and studio ' + + 'copies had converged on the kernel\'s structured `{ type, pattern }` shape) — ' + 'promised lazy plugin activation ("plugins remain dormant until an activation event ' + 'fires") that no runtime in objectstack, cloud, cloud-v1 or objectui ever ' + "implemented: nothing anywhere read the key, every plugin activates immediately, and " @@ -31,8 +32,11 @@ export const entry: SemanticMigration = { + 'with a guidance prescription (as are its former VS Code-flavoured aliases ' + '`activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / ' + '`ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys ' - + '(#3950: an exported schema with no consumer is read as a capability). #4657. ' - + 'SUPERSEDED ON THE KERNEL SIDE by #4834 (same unreleased major): the whole ' + + '(the lesson of the unwired plugin sandboxing / integrity / approval config removed ' + + 'before this: an exported schema with no consumer is read as a capability). Both keys ' + + 'took ADR-0049\'s REMOVE answer, not ENFORCE, while protocol 17 was still unreleased. ' + + 'SUPERSEDED ON THE KERNEL SIDE by the maintainer\'s REMOVE ' + + 'ruling on the rest of the plugin-runtime family (same unreleased major): the whole ' + '`DynamicLoadRequest` shape — and the rest of the plugin-runtime family with it — ' + 'was removed, which took this key\'s `retiredKey()` tombstone with it. That is ' + 'strictly stronger than the tombstone, not weaker: there is no longer a ' @@ -43,7 +47,8 @@ export const entry: SemanticMigration = { acceptanceCriteria: 'No `defineStudioPlugin` input authors `activationEvents` — authoring it is an ' + 'unknown key on the strict studio manifest and a parse error carrying the ' - + 'prescription. On the kernel side the stronger #4834 criterion applies instead: ' + + 'prescription. On the kernel side the stronger criterion of the plugin-runtime ' + + 'family\'s removal applies instead: ' + 'there is no `DynamicLoadRequest` type or schema left to author it into at all. No ' + 'code imports `ActivationEventSchema` / `ActivationEvent` from ' + '`@objectstack/spec/kernel` or `@objectstack/spec/studio` (TS2305 after upgrade). ' diff --git a/packages/spec/src/migrations/entries/semantic/17.plugin-manifest-loading-retired.ts b/packages/spec/src/migrations/entries/semantic/17.plugin-manifest-loading-retired.ts index 580e0e314b6..dc6b9dbb553 100644 --- a/packages/spec/src/migrations/entries/semantic/17.plugin-manifest-loading-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/17.plugin-manifest-loading-retired.ts @@ -22,15 +22,16 @@ export const entry: SemanticMigration = { + 'and queried by nothing, so it refuses no operation. Neither surface confines a ' + 'plugin today — do not author either one expecting isolation', reason: - 'ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a ' + 'ADR-0049 enforce-or-remove; the maintainer ruled REMOVE on 2026-08-04, on condition ' + + 'that a bare-name sweep of cloud and objectui came back clean first. The block declared a ' + 'complete plugin loading policy and NOTHING read it. A bare-name scan of all three ' + 'repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), ' + 'each with a control probe proving the scan saw the tree — put every hit inside ' + '`packages/spec` itself: this module\'s own declaration, its own unit tests, the ' + '`Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero ' + 'readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key ' - + 'parsed, entered the manifest, and changed nothing — #3950, at the scale of a whole ' - + 'block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared ' + + 'parsed, entered the manifest, and changed nothing — an exported schema with no ' + + 'consumer, read as a capability, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared ' + 'process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ' + 'ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform ' + 'isolates plugins, wrote the config, and received a clean parse and zero isolation. An ' diff --git a/packages/spec/src/migrations/entries/semantic/17.plugin-runtime-family-retired.ts b/packages/spec/src/migrations/entries/semantic/17.plugin-runtime-family-retired.ts index 659cdc88cf6..1897bb610b1 100644 --- a/packages/spec/src/migrations/entries/semantic/17.plugin-runtime-family-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/17.plugin-runtime-family-retired.ts @@ -23,20 +23,24 @@ export const entry: SemanticMigration = { + 'a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most ' + 'inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the ' + 'published IDE bundle as proof the platform hot-loads plugins and constructs a ' - + 'request that parses clean and is received by nobody (#3950: an exported schema ' - + 'with no consumer is read as a capability). The #3896 follow-up removed this ' - + "module's discovery/sandbox config island and left these five in place explicitly — " + + 'request that parses clean and is received by nobody (an exported schema with no ' + + 'consumer is read as a capability). The earlier removal of this module\'s ' + + 'discovery/sandbox config island — plugin sandboxing, integrity and approval settings ' + + 'that nothing read — left these five in place explicitly: ' + '"operation contracts, not security promises; the enforce-or-remove call on them is ' + 'a design decision rather than a correction" — but that suspension lived only in a ' - + 'changeset paragraph with no issue carrying it. #4834 is that decision, answered ' - + 'REMOVE. `experimental` was considered and rejected: it is only `.describe()` prose ' + + 'changeset paragraph with no issue carrying it. The maintainer\'s ruling of ' + + '2026-08-03 is that decision, answered REMOVE: hot loading is a real future ' + + 'capability, but nothing is being built and nothing pulls it, and when it is built ' + + 'its vocabulary enters the schema with the implementation. `experimental` was ' + + 'considered and rejected: it is only `.describe()` prose ' + 'and cannot stop an import, the weakest of the three ADR-0049 channels. None of the ' + 'five is stored metadata — they are root request/result payload shapes embedded in ' + 'no parent schema and parsed against no metadata document — so no `sys_metadata` ' + 'row can carry one and there is no source for the D2 chain to rewrite; this entry ' + 'is the D3 record. The removal also subsumes the kernel half of ' - + '`plugin-activation-events-retired` (#4657): that tombstone goes with the shape ' - + 'that carried it. ADR-0049, #4834.', + + '`plugin-activation-events-retired`: that tombstone goes with the shape ' + + 'that carried it. ADR-0049.', acceptanceCriteria: 'No code imports `DynamicLoadRequestSchema`, `DynamicUnloadRequestSchema`, ' + '`DynamicPluginResultSchema`, `PluginSourceSchema`, `DynamicPluginOperationSchema` ' diff --git a/packages/spec/src/migrations/entries/semantic/17.ui-interaction-config-family-retired.ts b/packages/spec/src/migrations/entries/semantic/17.ui-interaction-config-family-retired.ts index 5613fe209d0..4dd530a47ea 100644 --- a/packages/spec/src/migrations/entries/semantic/17.ui-interaction-config-family-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/17.ui-interaction-config-family-retired.ts @@ -16,7 +16,8 @@ export const entry: SemanticMigration = { + 'Offline is a platform capability, and its vocabulary belongs on the sync engine that ' + 'owns the queue, the conflict policy and the cache — none of which exists yet. Delete ' + 'the import and the value. Whichever of these earns real product pull returns WITH its ' - + 'own vocabulary and its executor, the #4910 way, not by un-retiring a declaration)', + + 'own vocabulary and its executor — the way inbound rate limiting came back, as a new ' + + 'key carrying only what its executor consumes — not by un-retiring a declaration)', reason: 'Five `@objectstack/spec/ui` modules declared a full interaction-configuration ' + 'vocabulary — 22 `z.object` sites across touch/gesture, drag-and-drop, ' @@ -38,17 +39,26 @@ export const entry: SemanticMigration = { + 'carrier flipped all 21 — so unreachability was a fact about the graph, not a broken ' + 'walker; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud ' + 'outside these modules\' own unit tests. objectui holds TYPE re-exports and parity ' - + 'ratchets, never validators, and says so (#2561). The 2026-08-04 ruling weighed ' + + 'ratchets, never validators, and says so (its types package deliberately dropped the ' + + 'spec/ui zod-validator re-exports and keeps type-only ones). The 2026-08-04 ruling ' + + 'retired the family — touch, drag-and-drop, keyboard and motion are renderer built-in ' + + 'behaviour and offline belongs to a sync engine, none of it per-page metadata — and weighed ' + 'wiring a carrier key (option B) and rejected it: that is a feature with a renderer ' + 'behind it, not ledger clean-up. It also weighed tightening the shapes to ' + '`strictObject` and rejected that explicitly — strictness is a property of a PARSE and ' + 'there is no parse, so it would spend a breaking change to leave "a precisely ' - + 'validated dead slot, the more convincing lie" (#4583). Because there was no carrier ' + + 'validated dead slot, the more convincing lie" (the lesson of the datasource capability ' + + 'flags: `readOnly` was precisely validated and read by nothing, while a shipped example ' + + 'called a datasource a read replica and wrote through it). Because there was no carrier ' + 'key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 ' - + 'conversion to rewrite: this entry is the D3 record, the same route 3 as #4834 (kernel ' - + 'plugin-runtime family) and #4938 (`HttpServerConfig`). ⚠️ Not to be confused with ' - + '#5021, which retired the THEME `animation` block — a different file, different defs, ' - + 'and that one did have a carrier key and therefore a tombstone. ADR-0049, #4988.', + + 'conversion to rewrite: this entry is the D3 record, the same route 3 as ' + + '`plugin-runtime-family-retired` (the kernel plugin-runtime family) and the ' + + '`HttpServerConfig` retirement (seven keys no runtime read and no authoring door ' + + 'reached, retired with their container). ⚠️ Not to be confused with the theme-token ' + + 'retirement (theme-driven typography is not a near-term capability, so nine token ' + + 'groups nothing consumed were retired), which retired the THEME `animation` block — a ' + + 'different file, different defs, and that one did have a carrier key and therefore a ' + + 'tombstone. ADR-0049.', acceptanceCriteria: 'No code imports any of the 64 retired names from `@objectstack/spec` or ' + '`@objectstack/spec/ui` — `TouchTargetConfig(Schema)`, `GestureType(Schema)`, ' diff --git a/packages/spec/src/migrations/entries/semantic/17.ui-notification-action-embed-config-retired.ts b/packages/spec/src/migrations/entries/semantic/17.ui-notification-action-embed-config-retired.ts index c88c9e71eb3..0871aab7aee 100644 --- a/packages/spec/src/migrations/entries/semantic/17.ui-notification-action-embed-config-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/17.ui-notification-action-embed-config-retired.ts @@ -15,7 +15,9 @@ export const entry: SemanticMigration = { + 'through a new ADR — carrier key and renderer first, vocabulary second)', reason: 'Both shapes were published `@objectstack/spec/ui` vocabulary with NO AUTHORING DOOR. ' - + '#4001 批 14 measured them three ways on 2026-08-03 and this retirement re-ran all ' + + 'The v17 unknown-key strictness sweep (its ui/ batch 14), which measured each ui/ ' + + 'file for an authoring door before closing any shape, measured them three ways on ' + + '2026-08-03 and this retirement re-ran all ' + 'three against `origin/main` before removing anything, each with a positive control ' + 'that passed in the same run: (1) CARRIER — no schema in `packages/spec/src` declared ' + 'a key of either type (`ui/notification.zod`\'s only non-test importer was the ' @@ -29,19 +31,24 @@ export const entry: SemanticMigration = { + 'itself all resolved `root-graph` in the same run and an injected synthetic carrier ' + 'flipped both; (3) PARSE — zero `.parse()` in objectstack, cloud or objectui outside ' + 'their own unit tests. So nobody could author one and nothing ever validated one: ' - + 'the #3950 shape, an exported schema with no consumer read as a capability, and the ' + + 'the shape of the unwired plugin sandboxing config removed before it, an exported ' + + 'schema with no consumer read as a capability, and the ' + 'ADR-0033 trap where an AI author takes `EmbedConfigSchema` in the published bundle ' + 'as proof the platform serves iframes. Neither is stored metadata and neither has a ' + 'carrier, so no `sys_metadata` row can hold one and there is no source for the D2 ' - + 'chain to rewrite; this entry is the D3 record. 批 14 deliberately did NOT close them ' - + 'with `.strict()` — strictness is a property of a PARSE, and closing a shape nothing ' - + 'parses buys only "a precisely-validated dead slot, the more convincing lie" (#4583) ' - + '— and filed the disposition as #5015, ruled REMOVE on 2026-08-04. Each was orphaned ' - + 'by an earlier retirement one level up: `NotificationAction` lost its wrappers at ' - + '#4610 (`NotificationSchema` / `NotificationConfigSchema`, the #4535 C3 dual-source ' - + 'cleanup — that retirement\'s published "zero consumers" evidence was later falsified ' - + 'for objectui and is corrected on `ui/notification.zod`\'s tombstone; the removal ' - + 'itself stands, #5781), and `EmbedConfig` lost its key at 17.0.0 when the 2026-06 ' + + 'chain to rewrite; this entry is the D3 record. That batch deliberately did NOT close ' + + 'them with `.strict()` — strictness is a property of a PARSE, and closing a shape nothing ' + + 'parses buys only "a precisely-validated dead slot, the more convincing lie" (the lesson ' + + 'of the datasource capability flags, whose `readOnly` was precisely validated and read ' + + 'by nothing) — and left the disposition to ADR-0049\'s enforce-or-remove, which came ' + + 'back REMOVE on 2026-08-04: a dead surface with no authoring door retires ' + + 'implementation-first, as three same-shape rulings that week had already decided. Each ' + + 'was orphaned by an earlier retirement one level up: `NotificationAction` lost its ' + + 'wrappers when the dual-source cleanup removed the `./ui` copies of `NotificationSchema` ' + + '/ `NotificationConfigSchema` (the same names declared differently on other entry ' + + 'points) — that retirement\'s published "zero consumers" evidence was later ' + + 'falsified for objectui, which re-exported both names, and is corrected on ' + + '`ui/notification.zod`\'s tombstone; the removal itself stands — and `EmbedConfig` lost its key at 17.0.0 when the 2026-06 ' + 'liveness audit retired `App.embed` (no iframe route ever read it) — that key still ' + 'stands as a `retiredKey()` tombstone in `app.zod.ts`, so an author who wrote the KEY already ' + 'meets a prescription; this removes the value shape that outlived it. ⚠️ The ' @@ -51,7 +58,7 @@ export const entry: SemanticMigration = { + 'enums. objectui consumed `NotificationActionSchema.shape.variant` as a VOCABULARY ' + '(never a parse) to pin its own hand-written `NotificationActionButton` interface — ' + 'which is exactly why "has a consumer" never meant "has an authoring door" here; that ' - + 'pin is adapted objectui-side when it refreshes this dependency. ADR-0049, #5015.', + + 'pin is adapted objectui-side when it refreshes this dependency. ADR-0049.', acceptanceCriteria: 'No code imports `NotificationActionSchema`, `NotificationAction`, `EmbedConfigSchema` ' + 'or `EmbedConfig` from `@objectstack/spec` or `@objectstack/spec/ui` — both are ' diff --git a/packages/spec/src/migrations/entries/semantic/17.ui-widget-i18n-family-retired.ts b/packages/spec/src/migrations/entries/semantic/17.ui-widget-i18n-family-retired.ts index e690cd8ac7f..32a1e8aa621 100644 --- a/packages/spec/src/migrations/entries/semantic/17.ui-widget-i18n-family-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/17.ui-widget-i18n-family-retired.ts @@ -14,7 +14,9 @@ export const entry: SemanticMigration = { + 'widget is still named the same way it always was: `field.widget` is a plain string ' + 'naming a component the RENDERER has registered, and objectui\'s registry has always ' + 'carried its own runtime manifest for that (`RuntimeWidgetManifest` / ' - + '`RuntimeWidgetSource` in `@object-ui/types`, objectui#3161 / #4115), which models ' + + '`RuntimeWidgetSource` in `@object-ui/types`, renamed off the spec\'s names under ' + + 'objectui\'s rule that a symbol named like a spec export must import it or take a name ' + + 'of its own), which models ' + 'different keys and never derived from these. For localisation: write the ' + 'default-language string on `label` / `description` — the framework generates the ' + 'translation key at registration time from the naming convention — and put ' @@ -40,22 +42,27 @@ export const entry: SemanticMigration = { + 'own unit tests. `NumberFormat` / `DateFormat` DID have a carrier key ' + '(`LocaleConfig.numberFormat` / `.dateFormat`) but the carrier was itself doorless, ' + 'so the subtree was `no door` rather than `no gate` and goes whole — leaving the two ' - + 'leaves behind would strand exported schemas with no consumer (#3950). ' + + 'leaves behind would strand exported schemas with no consumer, which an author reads ' + + 'as a capability. ' + '`I18nObjectSchema` was additionally superseded by its own file-neighbour: ' + '`I18nLabelSchema`\'s documentation already says translation keys are generated at ' + 'registration time and translations live in translation files, and the live ' + 'translation surface is `system/translation.zod.ts`, which uses none of these shapes. ' + 'The 2026-08-06 ruling weighed giving them a carrier (option B) and rejected it: that ' + 'is a feature with a registry and a renderer behind it, not ledger clean-up. ' - + 'Tightening them to `strictObject` was rejected earlier and explicitly (#4001 批 16) ' - + '— strictness is a property of a PARSE and there is no parse, so it would spend a ' + + 'Tightening them to `strictObject` was rejected earlier and explicitly, by the batch of ' + + 'the v17 unknown-key strictness sweep that measured this file as having no authoring ' + + 'door — strictness is a property of a PARSE and there is no parse, so it would spend a ' + 'breaking change to leave "a precisely validated dead slot, the more convincing lie" ' - + '(#4583). With no carrier key there is nothing to tombstone and no `sys_metadata` row ' + + '(the lesson of the datasource capability flags, whose `readOnly` was precisely ' + + 'validated and read by nothing). With no carrier key there is nothing to tombstone and no `sys_metadata` row ' + 'or source file for a D2 conversion to rewrite: this entry is the D3 record, route 3, ' - + 'the same shape as #4988 (the ui/ interaction config family), #4834 (kernel ' - + 'plugin-runtime family) and #4938 (`HttpServerConfig`). ' - + '⚠️ `WidgetManifest.performance`\'s own `retiredKey()` tombstone (#3896 close-out) is ' - + 'SUBSUMED here, the #4657/#4834 way: it goes with the shape that carried it, which is ' + + 'the same shape as `ui-interaction-config-family-retired`, ' + + '`plugin-runtime-family-retired` and the `HttpServerConfig` retirement. ' + + '⚠️ `WidgetManifest.performance`\'s own `retiredKey()` tombstone (left by the ' + + 'close-out sweep that removed the inert `performance` keys no renderer applied) is ' + + 'SUBSUMED here, the way the kernel `activationEvents` tombstone went with the removed ' + + 'plugin-runtime family: it goes with the shape that carried it, which is ' + 'strictly stronger than the tombstone, because there is no longer a manifest to ' + 'author the key INTO. ' + '⚠️ One of the nine widget sites is deliberately NOT retired. ' @@ -63,12 +70,13 @@ export const entry: SemanticMigration = { + 'authorable metadata (it never appeared in `authorable-surface/` or ' + '`json-schema.manifest/` — its `onChange` is a `z.function()`), so "zero parse" is its ' + 'design and not its defect, and it acquired a live cross-repo compile-time consumer ' - + 'one day before 批 16 measured: objectui PR #3289 (2026-08-03) renamed ' + + 'one day before that sweep batch measured: an objectui fix of 2026-08-03, made to follow the ' + + 'spec, renamed ' + '`@object-ui/fields`\' validation slot onto the spec\'s `error` with no alias, the ' + 'form renderer began producing it, and ' + '`packages/fields/src/__tests__/spec-symbol-batch7.test.ts` pins the shape against ' + '`import type { FieldWidgetProps } from \'@objectstack/spec/ui\'` as an intentional ' - + 'tripwire. Re-verified on objectui `origin/main` 2026-08-07. ADR-0049, #5055.', + + 'tripwire. Re-verified on objectui `origin/main` 2026-08-07. ADR-0049.', acceptanceCriteria: 'No code imports `WidgetManifest(Schema|Parsed)`, `WidgetLifecycle(Schema)`, ' + '`WidgetEvent(Schema|Parsed)`, `WidgetProperty(Schema|Parsed)`, ' @@ -84,8 +92,9 @@ export const entry: SemanticMigration = { + 'resolve on `@objectstack/spec/ui` and are asserted to. ⚠️ objectui needs a companion ' + 'PR in the same window: `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts` ' + 'asserts the spec STILL owns `WidgetManifest` / `WidgetSource` (it is the ' - + '"a workaround should not outlive its reason" half of the objectui#3169 tripwire, ' - + 'designed to go red exactly here), and `packages/types/src/widget.ts`\'s ' + + '"a workaround should not outlive its reason" half of the rename tripwire objectui ' + + 'added when it stopped declaring symbols under names the spec owns, designed to go red ' + + 'exactly here), and `packages/types/src/widget.ts`\'s ' + '"Renamed off the spec\'s `WidgetManifest` name" comments now point at names that no ' + 'longer exist. Both are prescribed responses to this removal, not collateral damage.', }; diff --git a/packages/spec/src/migrations/entries/semantic/18.plugin-auto-restart-never-reinitialised.ts b/packages/spec/src/migrations/entries/semantic/18.plugin-auto-restart-never-reinitialised.ts index 5c3da787bc2..a8dba242cd0 100644 --- a/packages/spec/src/migrations/entries/semantic/18.plugin-auto-restart-never-reinitialised.ts +++ b/packages/spec/src/migrations/entries/semantic/18.plugin-auto-restart-never-reinitialised.ts @@ -20,8 +20,11 @@ export const entry: SemanticMigration = { + 'process — whichever level actually owns the plugin\'s lifetime. The ' + 'monitor reports; it does not act.', reason: - 'ADR-0049 enforce-or-remove, applied one class over from #12428 and ' - + '#12340 in the same host-driven lifecycle library, and for a sharper ' + 'ADR-0049 enforce-or-remove, applied one class over from the two ' + + 'hot-reload retirements in the same host-driven lifecycle library — the ' + + 'file-watching placeholder whose `startWatching` logged success while ' + + 'watching nothing, and the `\'disk\'` / `\'distributed\'` state strategies ' + + 'that fell back to memory in silence — and for a sharper ' + 'reason than either: this key HAD a reader that acted, and what it did ' + 'was not what the key declared. `attemptRestart` called ' + '`plugin.destroy()` and stopped there. The comment above the call read ' @@ -35,7 +38,8 @@ export const entry: SemanticMigration = { + 'plugin was therefore `healthy`, reproduced at ee3595cefd with ' + '`successThreshold: 3` as failed -> recovering (destroyed=1, alive=false) ' + '-> recovering -> recovering -> healthy (destroyed=1, alive=false). ' - + '#11955 made that MORE convincing rather than less, because reaching ' + + 'The fix that made `successThreshold` bind from every status that records ' + + 'a failure made that MORE convincing rather than less, because reaching ' + '`healthy` now costs `successThreshold` CONSECUTIVE passing rounds, so ' + 'the plugin has to earn a declared number of passes to be misreported. ' + 'Meanwhile `restartAttempts` was incremented as though a restart had ' @@ -50,7 +54,9 @@ export const entry: SemanticMigration = { + 'call (positive control: the same scan resolves five real non-test ' + '`plugin.destroy()` call sites, so it sees lifecycle drivers). Building ' + 'that API for a caller that does not exist — no runtime constructs ' - + '`PluginHealthMonitor` (#11825) — is exactly the speculation ADR-0049\'s ' + + '`PluginHealthMonitor`, which is why the maintainer retired its declarative ' + + 'config container on 2026-08-25 and kept the classes as a host-driven ' + + 'library — is exactly the speculation ADR-0049\'s ' + 'staged decision names as the wrong default at this milestone, where the ' + 'shippable liability is the false promise and not the missing feature. ' + 'EXPERIMENTAL requires a roadmap, and a scan of the whole `docs/` ' @@ -61,10 +67,13 @@ export const entry: SemanticMigration = { + 'and "Backoff strategy for restart delays" have nothing left to be the ' + 'vocabulary OF — the same test that took `distributedConfig` out with ' + 'the `stateStrategy` value it was documented as being required for ' - + '(#12340). All three are TOMBSTONED rather than deleted, for #12428\'s ' - + 'reason: a key leaving a SURVIVING def has no route-3 exit, and ' + + '(ruled 2026-08-26: a vocabulary of nothing is not a vocabulary). All ' + + 'three are TOMBSTONED rather than deleted, for the reason the file-watching ' + + 'retirement recorded: a key leaving a SURVIVING def has no route-3 exit, and ' + '`PluginHealthCheckSchema` is not `.strict()`, so a bare deletion would ' - + 'be a silent strip (#3733, ADR-0104) — a milder form of the very defect ' + + 'be a silent strip (ADR-0104; an earlier field-key prune measured exactly ' + + 'that — the parse succeeded and the removed key was dropped without a ' + + 'word) — a milder form of the very defect ' + 'being retired. There is no D2 conversion, because `PluginHealthCheck` ' + 'is not an authorable surface: no metadata-type binding, stack ' + 'collection or manifest embed ever carried it, so there is no authored ' @@ -81,12 +90,15 @@ export const entry: SemanticMigration = { + 'gone with the `restartAttempts` counter, and a plugin that crosses ' + '`failureThreshold` is reported `degraded` / `unhealthy` / `failed` and ' + 'left running. The rest of the monitor is UNCHANGED: registration, ' - + 'periodic checks, the `timeout` race and its refd-timer guard (#4875), ' - + 'the two failure routes sharing counters (#11852), and ' + + 'periodic checks, the `timeout` race and its guard timer (kept ref\'d ' + + 'while the race is undecided, cleared the moment it settles), the two ' + + 'failure routes — a returned failure and a thrown or timed-out check — ' + + 'sharing one failure counter and one threshold comparison, and ' + '`successThreshold` binding from every status that records a failure ' - + '(#11955) all behave exactly as before — `recovering` is now written ' + + 'all behave exactly as before — `recovering` is now written ' + 'only by the success branch, which is the one writer that ever meant it. ' - + 'The #11825 keep still stands: `PluginHealthCheckSchema` still exports ' + + 'The maintainer\'s 2026-08-25 keep of the host-driven library still ' + + 'stands: `PluginHealthCheckSchema` still exports ' + 'from `./kernel` and `PluginHealthMonitor` still exports from ' + '`@objectstack/core` with its tests green.', }; diff --git a/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-contributes-dead-members-retired.ts b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-contributes-dead-members-retired.ts index a171aa91180..5f87aea464a 100644 --- a/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-contributes-dead-members-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-contributes-dead-members-retired.ts @@ -24,9 +24,10 @@ export const entry: SemanticMigration = { + "plugin auto-discovery (an `oclif` section in the plugin's own `package.json`; see " + '`cli-extension.zod.ts`)', reason: - 'ADR-0049 enforce-or-remove; #10724 (triage graded 2026-08-21, cloud precondition ' - + 'discharged 2026-08-24). #10627 measured, monorepo-wide and non-test with control probes, ' - + 'that the ENTIRE monorepo contains exactly one read of `manifest.contributes` — ' + 'ADR-0049 enforce-or-remove: the nine members retire together, once the cloud half of ' + + 'the census below had come back clean. A census, monorepo-wide and non-test with ' + + 'control probes, measured that the ENTIRE monorepo contains exactly one read of ' + + '`manifest.contributes` — ' + '`packages/objectql/src/engine.ts`, member `kinds` — so all nine members above parsed, ' + 'entered the manifest, and changed nothing. The census stands on three repos: objectstack ' + '(re-verified on current main at claim time), objectui (0 property reads; control: 63 ' @@ -46,8 +47,9 @@ export const entry: SemanticMigration = { + 'present: `os plugin build` runs `ManifestSchema.safeParse` and exits non-zero printing ' + 'the per-key tombstone prescription; TypeScript authors fail earlier still (each key is ' + 'typed `never`). `contributes.kinds` keeps parsing and registering ' - + '(`registry.registerKind`), and `contributes.routes` is untouched pending its own fork ' - + '(#10726). ⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: ' + + '(`registry.registerKind`), and `contributes.routes` is left to its own enforce-or-remove ' + + 'fork (since decided: retired, see `plugin-manifest-contributes-routes-retired`). ' + + '⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: ' + 'nothing ever read the nine members, so removing them removes no behaviour. A package ' + 'ALREADY INSTALLED whose stored manifest carries one degrades to a single ' + '`[metadata_spec_invalid]` log line at registration (the registry\'s `validate()` is a ' 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 index 7bb10077973..1783647954b 100644 --- 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 @@ -5,8 +5,8 @@ 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)', + 'manifest.contributes.routes (the one member the nine-member retirement deliberately ' + + 'left to its own fork; `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 ' @@ -14,23 +14,27 @@ export const entry: SemanticMigration = { + '`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)', + + 'once the declarative endpoint executor was built and the loud refusal of a ' + + 'non-empty `apis:` became execution)', reason: - 'ADR-0049 enforce-or-remove; #10726, maintainer ruling 2026-08-22 (Option B of the ' + 'ADR-0049 enforce-or-remove; 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 ' + + 'batch carrying the four-axis analysis): remove the key, and redirect every ' + + 'author-facing recommendation of it to the imperative `http.server` mount. A ' + + 'monorepo-wide census with control probes measured zero readers of the key: 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 ' + + 'author-facing corrections landed FIRST (the skill\'s decision table, the ' + + 'dispatcher protocol doc, ADR-0088:40 and app.mdx, each redirected to the ' + + 'imperative mount), and the two remaining teaching sites (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 first: a census of the cloud repository at 5b5925a found 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 ' diff --git a/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-dead-containers-retired.ts b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-dead-containers-retired.ts index 3db83d6a214..0ae01ca35a0 100644 --- a/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-dead-containers-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-dead-containers-retired.ts @@ -22,9 +22,9 @@ export const entry: SemanticMigration = { + '(ADR-0029 D7) injects navigation, and code-level extension lives in the plugin itself ' + '(`init`/`start`)', reason: - 'ADR-0049 enforce-or-remove; #11332 (triage graded 2026-08-23, cloud precondition ' - + 'discharged 2026-08-29 on #12400). #11332 measured, monorepo-wide and non-test with ' - + 'control probes, ZERO reads of each container itself, which settles all eight keys ' + 'ADR-0049 enforce-or-remove, dispatched once the cloud half of the census below came ' + + 'back clean. A census, monorepo-wide and non-test with control probes, measured ZERO ' + + 'reads of each container itself, which settles all eight keys ' + 'beneath them — a key cannot be read if the object holding it never is. The census ' + 'stands on three repos: objectstack (re-verified on current main at claim time; every ' + 'bare `.capabilities` hit classifies to a different surface — driver loader contracts, ' diff --git a/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-kind-globs-retired.ts b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-kind-globs-retired.ts index 7a1b954ee56..13b9a098a02 100644 --- a/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-kind-globs-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-kind-globs-retired.ts @@ -13,16 +13,17 @@ export const entry: SemanticMigration = { + 'discovery is ever wanted, it gets designed against that registry, not revived ' + 'here', reason: - 'ADR-0049 enforce-or-remove; #11169, maintainer ruling 2026-08-24 (「接受你的建议。」) on ' - + 'the aligned four-facet analysis. The sub-field was declared-but-unenforced on an ' + 'ADR-0049 enforce-or-remove; maintainer ruling 2026-08-24 (「接受你的建议。」) on ' + + 'the aligned four-facet analysis: remove, through the full ADR-0049 ceremony. The sub-field was declared-but-unenforced on an ' + 'authorable published surface: the schema promised that declaring `globs` "enables the ' + 'system to parse and validate new file types" (its own example: a BI plugin handling ' + '`*.report.ts`), and the platform accepted it, stored it, and served it back through ' + '`GET /metadata/kind` — while the discovery the description promised never ran, because ' + 'real glob-driven artifact discovery reads `filePatterns` off the metadata type registry ' + 'and `metadata-plugin.zod.ts` records outright that `contributes.kinds` does not extend ' - + 'it. Measured in PR #11168 and re-verified at claim time with the card\'s positive ' - + 'control: zero value reads anywhere (the only non-test occurrences of the path are the ' + + 'it. Measured by the engine-lane fix that made kind registration log its declared ' + + '`id` (which also found the `kind` bucket itself reachable through `GET /metadata/:type`), ' + + 'and re-verified at claim time with a positive control: zero value reads anywhere (the only non-test occurrences of the path are the ' + 'schema declaration and two type positions), and no in-repo manifest authors the key ' + 'outside test fixtures. Enforce was weighed and rejected on all four facets: it would ' + 'build a SECOND discovery channel parallel to `filePatterns` for a spelling with zero ' diff --git a/packages/spec/src/migrations/entries/semantic/18.plugin-security-scan-result-surface-retired.ts b/packages/spec/src/migrations/entries/semantic/18.plugin-security-scan-result-surface-retired.ts index c531c10f9f8..1055b5e4ccf 100644 --- a/packages/spec/src/migrations/entries/semantic/18.plugin-security-scan-result-surface-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/18.plugin-security-scan-result-surface-retired.ts @@ -24,9 +24,11 @@ export const entry: SemanticMigration = { + 'advertise diligence keeps the surviving securityContact and vulnerabilityDisclosure ' + 'blocks, which are contact terms rather than a verdict.', reason: - 'ADR-0049 enforce-or-remove; maintainer ruling 2026-09-07 on #15932 (director seat, decision batch #65, adopted verbatim 「同意」). ' - + 'This is the second half of the scanner retirement — issue 14919, a number since ' - + 'DELETED from the board, landed as PR #15930. That card retired PluginSecurityScanner — a ' + 'ADR-0049 enforce-or-remove; maintainer ruling 2026-09-07 (adopted verbatim 「同意」): ' + + 'retire the scan-result family and its securityScan sibling, because once the scanner ' + + 'was gone nothing so much as imported their types. ' + + 'This is the second half of the scanner retirement recorded as ' + + 'plugin-security-scanner-retired. That retirement removed PluginSecurityScanner — a ' + '@objectstack/core class that shipped as a SECURITY control and could not fail, whose ' + 'verdict was status "passed" for every plugin it was ever handed. The SCHEMAS the ' + 'scanner fed survived it, and the scanner had been their only importer of any kind (a ' @@ -59,10 +61,12 @@ export const entry: SemanticMigration = { + 'recorded as checked. Its two siblings were MEASURED rather than assumed, and the ' + 'record is corrected here: both were ALREADY GONE when that ruling was written. The ' + 'incident "malware" type was a member of system/IncidentCategory, and the whole ' - + 'incident-response family was retired by #15513 (maintainer ruling 2026-09-05 — two ' - + 'days BEFORE the 2026-09-07 ruling that made it conditional); see ' + + 'incident-response family was retired whole, with the training and change-management ' + + 'families (maintainer ruling 2026-09-05: not roadmapped, so retired rather than marked ' + + 'experimental — two days BEFORE the 2026-09-07 ruling that made it conditional); see ' + 'incident-response-family-retired. And marketplace-admin.zod.ts was deleted outright ' - + 'with the cloud subpath (#16526); see cloud-subpath-retired. Verified on this tree by ' + + 'with the cloud subpath (ruled 2026-09-07: cloud does not re-host the control-plane ' + + 'files it never consumed); see cloud-subpath-retired. Verified on this tree by ' + 'shape: both files return zero tree entries and no *.zod.ts names malware at all, ' + 'against a lit control where "scanning" still returns a live declaration in ' + 'marketplace.zod.ts. So the conditional question is ONE enum member wide, not three, ' @@ -70,8 +74,7 @@ export const entry: SemanticMigration = { + '⚠️ The out-of-repo consumer population is NOT MEASURED. @objectstack/spec is published, ' + 'so this removal is breaking for consumers no download, dependent or source telemetry ' + 'was consulted for — accepted as an input to the ruling, exactly as that retirement states ' - + 'of its own three exports, and not a reason to soften the removal. #15932, PR #15930 ' - + '(for the deleted issue 14919), ADR-0049, ADR-0087.', + + 'of its own three exports, and not a reason to soften the removal. ADR-0049, ADR-0087.', acceptanceCriteria: 'No source imports KernelSecurityScanResult, KernelSecurityVulnerability or either ' + 'Schema from @objectstack/spec/kernel: both defs are absent from the built kernel ' diff --git a/packages/spec/src/migrations/entries/semantic/18.plugin-security-scanner-retired.ts b/packages/spec/src/migrations/entries/semantic/18.plugin-security-scanner-retired.ts index 05630a29d5e..ec40b38033c 100644 --- a/packages/spec/src/migrations/entries/semantic/18.plugin-security-scanner-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/18.plugin-security-scanner-retired.ts @@ -20,8 +20,8 @@ export const entry: SemanticMigration = { + 'the GitHub Advisory Database, OSV — and treat an unaudited third-party plugin as ' + 'untrusted code.', reason: - 'ADR-0049 enforce-or-remove; maintainer ruling 2026-09-05 on #14919 (director summon #14, ' - + 'decision batch #42, ruled A: retire in three surfaces). The class shipped on ' + 'ADR-0049 enforce-or-remove; maintainer ruling 2026-09-05: retire the class and its two ' + + 'companion types with no replacement export, rather than repair it. The class shipped on ' + '`@objectstack/core`\'s public barrel as a SECURITY control and could not fail. `scan()` ' + 'composed five private scanners: four of them (`scanCode`, `scanMalware`, `scanLicenses`, ' + '`scanConfiguration`) allocated an empty issue array, logged and returned it with no code ' @@ -41,8 +41,10 @@ export const entry: SemanticMigration = { + 'per-instance Map discarded with the object), and hence no seam `applyConversionsToStored' + 'Item` would ever reach. The enforced channel is tsc, at the consumer\'s own import site; ' + 'for anyone it does not reach, this ledger entry and the generated upgrade guide are the ' - + 'only channel there is. That is the `contracts.IDataDriver.findStream` (#4484) and ' - + '`actor-user-roles-to-positions` (#6011) disposition — a TS/API contract, no stored ' + + 'only channel there is. That is the disposition of `contracts.IDataDriver.findStream` ' + + '(removed with no tombstone, because nothing parses a driver object) and of ' + + '`actor-user-roles-to-positions` (the `ctx.user` `roles` alias, closed at once on the ' + + 'maintainer\'s word rather than given a window) — a TS/API contract, no stored ' + 'source, no tombstone, tsc at the call site — applied to a surface one layer further out ' + 'than either: those are declared in `packages/spec`, this one only in `packages/core`. ' + '⚠️ The out-of-repo consumer population is NOT MEASURED. Zero constructors were found in ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-cloud-connection-widgets-unknown-keys-refused.ts b/packages/spec/src/migrations/entries/semantic/18.ui-cloud-connection-widgets-unknown-keys-refused.ts index 0f700b526bd..ccb3e40b0f5 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-cloud-connection-widgets-unknown-keys-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-cloud-connection-widgets-unknown-keys-refused.ts @@ -13,10 +13,15 @@ export const entry: SemanticMigration = { + 'not renamed. Node-level keys (`visibleWhen`, `id`, `style`, …) stay on the component ' + 'node, where the page runtime reads them.', reason: - 'These were two more instances of the #8691/#8744 class: console-registered widgets on ' + 'These were two more instances of the class already closed for `record:reference_rail` ' + + 'and then for `record:alert` / `record:quick_actions` / `record:history`, each by ' + + 'declaring a strict `ComponentPropsMap` row measured from the renderer\'s read points: ' + + 'console-registered widgets on ' + '`@objectstack/cloud-connection`\'s published Setup pages, reachable through the ' + 'component type union\'s open string arm, with registered renderers but no ' - + '`ComponentPropsMap` row — so the #5068 props gate\'s dispatch skipped them as ' + + '`ComponentPropsMap` row — so the props gate\'s dispatch (it parses `properties` ' + + 'against the type\'s row at publish and lint time, and skips a type with no row because ' + + 'the type union is open) skipped them as ' + 'unregistered and any authored key rode through every validator in silence. The new ' + 'rows are strict and EMPTY, measured from the renderers\' actual read points at the ' + 'objectui pin (not from the registrations\' declared-input lists): both registrations ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-form-field-length-malformed-refused.ts b/packages/spec/src/migrations/entries/semantic/18.ui-form-field-length-malformed-refused.ts index a2185320f15..4122205864e 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-form-field-length-malformed-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-form-field-length-malformed-refused.ts @@ -10,18 +10,22 @@ export const entry: SemanticMigration = { replacement: 'a positive-integer bound (>= 1), or no declaration at all ("no minimum" is ' + 'expressed by OMITTING `minLength`, never by `minLength: 0`). The row-level key is a ' + 'per-form override that can only NARROW what the referenced object field already ' - + 'declares (the object field surface tightened first: #11566/#11949) — so a malformed ' + + 'declares (the object field surface tightened first, to a positive integer, by ' + + 'maintainer rulings) — so a malformed ' + 'row value is deleted, and a bound that was actually wanted is re-declared as a ' + 'positive integer, or dropped in favour of the object field\'s own authoritative ' + 'declaration', reason: - '#12174: the form-field row carried the pre-#11566 shape — bare `z.number()` — after ' - + 'the object-field surface converged (`maxLength` #11566, `minLength` #11949, both ' + 'The form-field row still carried the object field\'s old shape — bare `z.number()` — ' + + 'after that surface converged (`maxLength` by the maintainer\'s 2026-08-24 ruling, ' + + '`minLength` by the 2026-08-25 one, which refused zero too: both ' + '`z.number().int().min(1)`). The row keys are LIVE, measured in objectui: the spec ' - + 'bridge (`packages/react/src/spec-bridge/bridges/form-view.ts` mapField, ' - + 'objectui#5898) and plugin-form (`sectionFields.ts` normalizeSectionField) both copy ' + + 'bridge (`packages/react/src/spec-bridge/bridges/form-view.ts` mapField, which maps ' + + 'every spec key or explains why it does not, so none is dropped in silence) and ' + + 'plugin-form (`sectionFields.ts` normalizeSectionField) both copy ' + 'them onto the runtime field, the console FormPage merges `override.maxLength ?? ' - + 'def.maxLength` onto the rendered input (objectui#5595), and the fields package ' + + 'def.maxLength` onto the rendered input (fixed so that a form\'s own bound wins over ' + + 'the object\'s, as its docstring promised), and the fields package ' + 'builds react-hook-form validation rules from `minLength`/`maxLength` — so ' + '`maxLength: 0` on a form row reached the DOM as an input that accepts nothing, and ' + 'the public-form resolve route (`GET /forms/:slug`) serves the rows verbatim to ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-form-field-precision-scale-integer-refused.ts b/packages/spec/src/migrations/entries/semantic/18.ui-form-field-precision-scale-integer-refused.ts index 8ce4499a096..c19936de519 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-form-field-precision-scale-integer-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-form-field-precision-scale-integer-refused.ts @@ -9,14 +9,16 @@ export const entry: SemanticMigration = { + 'non-integer or negative values (`scale: 2.5`, `precision: -1`)', replacement: 'a non-negative integer digit count, or no declaration at all. The row-level ' + 'key is a per-form override of the referenced object field\'s own declaration (that ' - + 'surface tightened first: #8321) — a malformed row value is deleted, and a count ' + + 'surface tightened first, to a non-negative integer) — a malformed row value is ' + + 'deleted, and a count ' + 'that was actually wanted is re-declared as a non-negative integer (`scale: 2.5` was ' + 'probably `2` or `3`)', reason: - '#12174: the form-field row carried the pre-#8321 shape — bare `z.number()` — after ' - + 'the object-field surface converged on `z.number().int().min(0)` for both digit ' + 'The form-field row still carried the object field\'s old shape — bare `z.number()` — ' + + 'after that surface converged on `z.number().int().min(0)` for both digit ' + 'counts. The row keys are LIVE, measured in objectui: the spec bridge ' - + '(`form-view.ts` mapField, objectui#5898) and plugin-form (`sectionFields.ts`) copy ' + + '(`form-view.ts` mapField, which maps every spec key or explains why it does not) and ' + + 'plugin-form (`sectionFields.ts`) copy ' + 'them onto the runtime field, `ObjectForm` derives the number input\'s step from ' + '`precision`, and the `NumberField` widget reads `scale` — so a malformed count ' + 'flowed into rendering arithmetic (`Math.pow(10, -precision)`) with no defined ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-form-view-predicate-features-root-refused.ts b/packages/spec/src/migrations/entries/semantic/18.ui-form-view-predicate-features-root-refused.ts index 69b333249be..b1b081cdbdd 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-form-view-predicate-features-root-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-form-view-predicate-features-root-refused.ts @@ -14,8 +14,9 @@ export const entry: SemanticMigration = { + 'is mechanical: a feature-flag gate and a record-state gate answer different ' + 'questions, so the author chooses which surface the gate belongs on.', reason: - 'objectstack#12665, ruled 2026-08-27 on objectui#6262 (option B — vocabulary ' - + 'narrowing): one authored form view is served on two kinds of route, and a ' + 'Ruled by the maintainer on 2026-08-27 (option B — vocabulary narrowing: a form view may ' + + 'not name `features.*` in a predicate, and the authoring door refuses it loudly): one ' + + 'authored form view is served on two kinds of route, and a ' + '`features.*` predicate got two verdicts from the same text. Inside an app ' + '(`/apps/:appName/*`) the root resolves against the real auth-config flags; on the ' + 'standalone form routes (`/forms/:name`, public `/f/:slug`) no app context exists, ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-list-view-groupbyfield-padded-refused.ts b/packages/spec/src/migrations/entries/semantic/18.ui-list-view-groupbyfield-padded-refused.ts index 71685074bf7..e198e902fec 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-list-view-groupbyfield-padded-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-list-view-groupbyfield-padded-refused.ts @@ -17,7 +17,9 @@ export const entry: SemanticMigration = { + 'the offending spelling verbatim, so the whitespace an author cannot see in an editor is ' + 'visible in the message, next to the name to write instead.', reason: - '#17499. All three keys were a bare `z.string()`, so a padded group-by name was valid ' + 'The same padded-name defect the grouping-level narrowing ' + + '(`ui-list-view-grouping-field-padded-refused`) refused, on the axis that one scoped out ' + + 'by name, and given the same refusal. All three keys were a bare `z.string()`, so a padded group-by name was valid ' + 'authored metadata all the way to the renderers. The name is a LOOKUP KEY on every row, ' + 'measured in objectui at `dda8f3815`: the kanban board resolves its lane as ' + '`laneField = groupByField || groupField || detectStatusField(objectDef)` and buckets cards ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-list-view-grouping-field-padded-refused.ts b/packages/spec/src/migrations/entries/semantic/18.ui-list-view-grouping-field-padded-refused.ts index 576a6cbe2ea..01ace1f325e 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-list-view-grouping-field-padded-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-list-view-grouping-field-padded-refused.ts @@ -13,7 +13,7 @@ export const entry: SemanticMigration = { + '`\'business_unit\'`. The refusal names the offending spelling verbatim, so the ' + 'whitespace an author cannot see in an editor is visible in the message.', reason: - '#17360, ruling C on objectui#7347 (maintainer 「其他同意」, decision batch #110 item 5): ' + 'Ruled by the maintainer on 2026-09-10 (「其他同意」): ' + 'refuse at the producer. `field` was a bare `z.string()`, so a padded grouping level ' + 'was valid authored metadata all the way to the renderers. Measured on objectui ' + '(M1-M11 with live controls): the projection harvester `collectGroupingFieldRefs` ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-mcp-connect-agent-unknown-keys-refused.ts b/packages/spec/src/migrations/entries/semantic/18.ui-mcp-connect-agent-unknown-keys-refused.ts index 1100d74d5af..1a27ceedbd4 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-mcp-connect-agent-unknown-keys-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-mcp-connect-agent-unknown-keys-refused.ts @@ -14,13 +14,18 @@ export const entry: SemanticMigration = { + 'not renamed. Node-level keys (`visibleWhen`, `id`, `style`, …) stay on the component ' + 'node, where the page runtime reads them.', reason: - 'This was a third instance of the #8691/#8744 class (#11575 closed the previous two): a ' + 'This was a third instance of the class closed for the `record:*` blocks by declaring a ' + + 'strict `ComponentPropsMap` row measured from the renderer\'s read points (the strict, ' + + 'empty `cloud-connection:panel` / `marketplace:installed-list` rows closed the previous ' + + 'two): a ' + 'console-registered widget on `@objectstack/mcp`\'s plugin-shipped Setup page, ' + 'reachable through the component type union\'s open string arm, with a registered ' - + 'renderer but no `ComponentPropsMap` row — so the #5068 props gate\'s dispatch ' + + 'renderer but no `ComponentPropsMap` row — so the props gate\'s dispatch (it parses ' + + '`properties` against the type\'s row, and skips a type with no row because the type ' + + 'union is open) ' + 'skipped it as unregistered, any authored key rode through every validator in ' - + 'silence, and door 3 of the mcp canonical-envelope gate (#12269) had to carry a ' - + 'standing exemption for the type. The new row is strict and EMPTY, measured from the ' + + 'silence, and door 3 of the canonical-envelope gate `@objectstack/mcp` was given for its ' + + 'shipped page had to carry a standing exemption for the type. The new row is strict and EMPTY, measured from the ' + 'renderer\'s actual read points at the objectui pin (not from the registration\'s ' + 'declared-input list): the registration ignores the component node entirely, so the ' + 'widget accepts no configuration at all, and an authored key is now a publish-time ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-page-size-positive-integer-refused.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-page-size-positive-integer-refused.ts index 238db0489f7..858cbae3c2f 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-page-size-positive-integer-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-page-size-positive-integer-refused.ts @@ -15,18 +15,20 @@ export const entry: SemanticMigration = { + 'authored to mean "no paging" is `showPagination: false` with no `pagination` bag, ' + "since the bag's PRESENCE is what enables paging)", reason: - '#19046: this door carried the pre-#7751 read-point shape — `pagination: z.unknown()` ' + 'This door still carried the shape it was given when the `object-*` blocks first got ' + + '`ComponentPropsMap` rows measured from their read points — `pagination: z.unknown()` ' + 'and `pageSize: z.number()` — after the view arm converged on ' + '`z.number().int().positive()`. So the SAME authored member carried two accept sets ' + 'and renderers read the looser one: `PaginationConfigSchema` (`view.zod.ts`) refuses ' + '`pageSize: 0` and pins that refusal by name, and every other `pageSize` the package ' + 'declares is bounded with its own throwing pin (`kernel/metadata-plugin.zod.ts`, ' + '`marketplace/marketplace.zod.ts`) — the component arm was the only one that ' - + 'accepted `0`. The value is LIVE: measured at objectui#9853, an authored ' + + 'accepted `0`. The value is LIVE: an objectui grid measurement found that an authored ' + '`pagination.pageSize: 0` reached `ObjectGrid`, went out on the wire as `$top: 0` ' + 'and rendered ZERO ROWS, with no grouping needed to trigger it, and it reached the ' - + 'renderer through this arm. objectui#9896 repaired the consumer half (a resolver at ' - + 'every read point, fail-soft, one loud diagnostic); this is the declaration half, ' + + 'renderer through this arm. objectui\'s grid plugin repaired the consumer half — it ' + + 'now refuses a non-positive page size at all three read points (one resolver, ' + + 'fail-soft, one loud diagnostic); this is the declaration half, ' + 'and it is not a prerequisite for that repair. ' + '⚠️ The `pagination` bag itself stays OPEN (`z.looseObject`): only the two members ' + 'whose value is a page size are bounded, and sibling keys parse and pass through ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-react-list-view-binding-aliases-retired.ts b/packages/spec/src/migrations/entries/semantic/18.ui-react-list-view-binding-aliases-retired.ts index 07b5322245a..e21b4168518 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-react-list-view-binding-aliases-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-react-list-view-binding-aliases-retired.ts @@ -5,7 +5,8 @@ import type { SemanticMigration } from '../../types.js'; export const entry: SemanticMigration = { id: 'ui-react-list-view-binding-aliases-retired', surface: '`kind:\'react\'` page source — `` and `` ' - + '(the react-tier overlay aliases #11284 had published as deprecated)', + + '(the react-tier overlay aliases published as deprecated when the react tier ' + + 'converged on the metadata-tier vocabulary)', replacement: '`` — ListViewSchema\'s own ' + '`data` data source and `type` view kind, the same two keys a metadata list view authors. ' + '`objectName="x"` → `data={{ provider: \'object\', object: \'x\' }}`; `viewType="kanban"` → ' @@ -15,11 +16,12 @@ export const entry: SemanticMigration = { 'A react page\'s source is a JSX string, not a keyed document: `objectstack migrate meta` ' + 'rewrites stored metadata by key and cannot rewrite props inside authored source, so the ' + 'move is by hand. The contract deprecated both aliases in favour of the metadata-tier ' - + 'spelling (#11284) while objectui\'s ListView still read only `objectName`, so the canonical ' + + 'spelling (maintainer ruling 2026-08-23: the react tier converges on the metadata-tier ' + + 'vocabulary, deprecating first) while objectui\'s ListView still read only `objectName`, so the canonical ' + 'spelling validated green and rendered an empty list. The consumer fold has landed (objectui ' + '`normalizeListViewSchema`, console pin a472b071: `data.provider === \'object\'` → ' + '`objectName`, and the author\'s `type` read for the view kind), and the maintainer ruled the ' - + 'aliases retired with no deprecation window (#14791, 2026-09-07). Writing either alias is now ' + + 'aliases retired with no deprecation window (2026-09-07). Writing either alias is now ' + 'a publish-time `react-prop-retired` error carrying this prescription — never a silent pass ' + 'on a key the renderer happens to still read.', acceptanceCriteria: diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-record-blocks-unknown-keys-refused.ts b/packages/spec/src/migrations/entries/semantic/18.ui-record-blocks-unknown-keys-refused.ts index 568b2b04f5f..29522c09c8b 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-record-blocks-unknown-keys-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-record-blocks-unknown-keys-refused.ts @@ -21,8 +21,10 @@ export const entry: SemanticMigration = { + 'declared until the renderer reads the contract spelling; `visibleWhen` / `visibility` ' + 'on the alert → `visible`; a locale map as history text → a literal string)', reason: - 'These were the four `record:*` components the #4001/#5068 gate could not reach after ' - + '#8691 closed the rail: each had a registered objectui renderer (and, bar ' + 'These were the four `record:*` components the component-props unknown-key gate (an ' + + 'authorable surface refuses a key it does not declare; for a component\'s `properties`, ' + + 'by parsing them against the type\'s `ComponentPropsMap` row) could not reach after ' + + 'the rail was given its strict row: each had a registered objectui renderer (and, bar ' + '`record:discussion`, a `PageComponentType` entry and a console palette slot) but no ' + '`ComponentPropsMap` row, so the props gate\'s dispatch skipped them as unregistered and ' + 'every authored key rode through. A typo\'d `severty` on the platform\'s own banner ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-reference-rail-unknown-keys-refused.ts b/packages/spec/src/migrations/entries/semantic/18.ui-reference-rail-unknown-keys-refused.ts index cd2e1349890..087a281d885 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-reference-rail-unknown-keys-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-reference-rail-unknown-keys-refused.ts @@ -15,7 +15,9 @@ export const entry: SemanticMigration = { + '`related` → `entries`; `object` → `objectName`; `label` → `title`; a `title` locale map ' + '→ a literal string, or omit it to keep the localized object label)', reason: - 'The rail was the `record:*` component the #4001/#5068 gate could not reach: it had a ' + 'The rail was the `record:*` component the component-props unknown-key gate (an ' + + 'authorable surface refuses a key it does not declare; for a component\'s `properties`, ' + + 'by parsing them against the type\'s `ComponentPropsMap` row) could not reach: it had a ' + 'registered renderer and a `PageComponentType` entry but no `ComponentPropsMap` row, so ' + 'the props gate\'s dispatch skipped it as unregistered and every authored key rode ' + 'through. Measured on 17.0.0 GA end to end: a planted entry `filter` passed tsc, ' diff --git a/packages/spec/src/migrations/migrations.test.ts b/packages/spec/src/migrations/migrations.test.ts index 9e40c941102..de7487b3708 100644 --- a/packages/spec/src/migrations/migrations.test.ts +++ b/packages/spec/src/migrations/migrations.test.ts @@ -157,7 +157,9 @@ describe('migration chain (ADR-0087 D3)', () => { it('finds the entry, and it still explains the #4610 orphaning (anti-vacuity)', () => { expect(entry()).toBeDefined(); - expect(entry()!.reason).toMatch(/#4610/); + // The orphaning is stated in words, not by tracker number: the reason is + // printed to the author by `os migrate meta`. + expect(entry()!.reason).toMatch(/dual-source cleanup removed the `\.\/ui` copies/); expect(entry()!.reason).toMatch(/NotificationConfigSchema/); }); @@ -171,7 +173,7 @@ describe('migration chain (ADR-0087 D3)', () => { it('names the correction and keeps the removal itself standing', () => { const r = entry()!.reason; expect(r).toMatch(/falsified/); - expect(r).toMatch(/#5781/); + expect(r).toMatch(/objectui, which re-exported both names/); // ⛔ A correction to the evidence is not an un-retirement. expect(r).toMatch(/removal itself stands/); }); diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index ae6dc6ed352..ab26922f448 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -3551,7 +3551,8 @@ const step17: MigrationStep = { + 'ADR-0049 through a new ADR, with a vocabulary its executor actually honours)', reason: 'Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary ' - + 'they embedded (`onCommand` / `onRoute` / … / `onView` after the #4653 convergence) — ' + + 'they embedded (`onCommand` / `onRoute` / … / `onView`, once the kernel and studio ' + + 'copies had converged on the kernel\'s structured `{ type, pattern }` shape) — ' + 'promised lazy plugin activation ("plugins remain dormant until an activation event ' + 'fires") that no runtime in objectstack, cloud, cloud-v1 or objectui ever ' + "implemented: nothing anywhere read the key, every plugin activates immediately, and " @@ -3569,8 +3570,11 @@ const step17: MigrationStep = { + 'with a guidance prescription (as are its former VS Code-flavoured aliases ' + '`activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / ' + '`ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys ' - + '(#3950: an exported schema with no consumer is read as a capability). #4657. ' - + 'SUPERSEDED ON THE KERNEL SIDE by #4834 (same unreleased major): the whole ' + + '(the lesson of the unwired plugin sandboxing / integrity / approval config removed ' + + 'before this: an exported schema with no consumer is read as a capability). Both keys ' + + 'took ADR-0049\'s REMOVE answer, not ENFORCE, while protocol 17 was still unreleased. ' + + 'SUPERSEDED ON THE KERNEL SIDE by the maintainer\'s REMOVE ' + + 'ruling on the rest of the plugin-runtime family (same unreleased major): the whole ' + '`DynamicLoadRequest` shape — and the rest of the plugin-runtime family with it — ' + 'was removed, which took this key\'s `retiredKey()` tombstone with it. That is ' + 'strictly stronger than the tombstone, not weaker: there is no longer a ' @@ -3581,7 +3585,8 @@ const step17: MigrationStep = { acceptanceCriteria: 'No `defineStudioPlugin` input authors `activationEvents` — authoring it is an ' + 'unknown key on the strict studio manifest and a parse error carrying the ' - + 'prescription. On the kernel side the stronger #4834 criterion applies instead: ' + + 'prescription. On the kernel side the stronger criterion of the plugin-runtime ' + + 'family\'s removal applies instead: ' + 'there is no `DynamicLoadRequest` type or schema left to author it into at all. No ' + 'code imports `ActivationEventSchema` / `ActivationEvent` from ' + '`@objectstack/spec/kernel` or `@objectstack/spec/studio` (TS2305 after upgrade). ' @@ -3607,15 +3612,16 @@ const step17: MigrationStep = { + 'and queried by nothing, so it refuses no operation. Neither surface confines a ' + 'plugin today — do not author either one expecting isolation', reason: - 'ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04 on #4914. The block declared a ' + 'ADR-0049 enforce-or-remove; the maintainer ruled REMOVE on 2026-08-04, on condition ' + + 'that a bare-name sweep of cloud and objectui came back clean first. The block declared a ' + 'complete plugin loading policy and NOTHING read it. A bare-name scan of all three ' + 'repos — objectstack, cloud (measured 2026-08-09) and objectui (measured at pickup), ' + 'each with a control probe proving the scan saw the tree — put every hit inside ' + '`packages/spec` itself: this module\'s own declaration, its own unit tests, the ' + '`Manifest.loading` embed and the generated artifacts. `manifest.loading.*` had zero ' + 'readers in `packages/core`, `packages/runtime` and `packages/metadata`. So the key ' - + 'parsed, entered the manifest, and changed nothing — #3950, at the scale of a whole ' - + 'block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared ' + + 'parsed, entered the manifest, and changed nothing — an exported schema with no ' + + 'consumer, read as a capability, at the scale of a whole block. What made it outrank ordinary inert-key cleanup is `sandboxing`: it declared ' + 'process / vm / iframe / web-worker isolation, IPC transports and an `allowedServices` ' + 'ACL, so an AI author (ADR-0033) reading that vocabulary concluded the platform ' + 'isolates plugins, wrote the config, and received a clean parse and zero isolation. An ' @@ -3669,20 +3675,24 @@ const step17: MigrationStep = { + 'a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most ' + 'inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the ' + 'published IDE bundle as proof the platform hot-loads plugins and constructs a ' - + 'request that parses clean and is received by nobody (#3950: an exported schema ' - + 'with no consumer is read as a capability). The #3896 follow-up removed this ' - + "module's discovery/sandbox config island and left these five in place explicitly — " + + 'request that parses clean and is received by nobody (an exported schema with no ' + + 'consumer is read as a capability). The earlier removal of this module\'s ' + + 'discovery/sandbox config island — plugin sandboxing, integrity and approval settings ' + + 'that nothing read — left these five in place explicitly: ' + '"operation contracts, not security promises; the enforce-or-remove call on them is ' + 'a design decision rather than a correction" — but that suspension lived only in a ' - + 'changeset paragraph with no issue carrying it. #4834 is that decision, answered ' - + 'REMOVE. `experimental` was considered and rejected: it is only `.describe()` prose ' + + 'changeset paragraph with no issue carrying it. The maintainer\'s ruling of ' + + '2026-08-03 is that decision, answered REMOVE: hot loading is a real future ' + + 'capability, but nothing is being built and nothing pulls it, and when it is built ' + + 'its vocabulary enters the schema with the implementation. `experimental` was ' + + 'considered and rejected: it is only `.describe()` prose ' + 'and cannot stop an import, the weakest of the three ADR-0049 channels. None of the ' + 'five is stored metadata — they are root request/result payload shapes embedded in ' + 'no parent schema and parsed against no metadata document — so no `sys_metadata` ' + 'row can carry one and there is no source for the D2 chain to rewrite; this entry ' + 'is the D3 record. The removal also subsumes the kernel half of ' - + '`plugin-activation-events-retired` (#4657): that tombstone goes with the shape ' - + 'that carried it. ADR-0049, #4834.', + + '`plugin-activation-events-retired`: that tombstone goes with the shape ' + + 'that carried it. ADR-0049.', acceptanceCriteria: 'No code imports `DynamicLoadRequestSchema`, `DynamicUnloadRequestSchema`, ' + '`DynamicPluginResultSchema`, `PluginSourceSchema`, `DynamicPluginOperationSchema` ' @@ -4369,7 +4379,8 @@ const step17: MigrationStep = { + 'Offline is a platform capability, and its vocabulary belongs on the sync engine that ' + 'owns the queue, the conflict policy and the cache — none of which exists yet. Delete ' + 'the import and the value. Whichever of these earns real product pull returns WITH its ' - + 'own vocabulary and its executor, the #4910 way, not by un-retiring a declaration)', + + 'own vocabulary and its executor — the way inbound rate limiting came back, as a new ' + + 'key carrying only what its executor consumes — not by un-retiring a declaration)', reason: 'Five `@objectstack/spec/ui` modules declared a full interaction-configuration ' + 'vocabulary — 22 `z.object` sites across touch/gesture, drag-and-drop, ' @@ -4391,17 +4402,26 @@ const step17: MigrationStep = { + 'carrier flipped all 21 — so unreachability was a fact about the graph, not a broken ' + 'walker; (3) zero `.parse()` / `.safeParse()` in objectstack, objectui or cloud ' + 'outside these modules\' own unit tests. objectui holds TYPE re-exports and parity ' - + 'ratchets, never validators, and says so (#2561). The 2026-08-04 ruling weighed ' + + 'ratchets, never validators, and says so (its types package deliberately dropped the ' + + 'spec/ui zod-validator re-exports and keeps type-only ones). The 2026-08-04 ruling ' + + 'retired the family — touch, drag-and-drop, keyboard and motion are renderer built-in ' + + 'behaviour and offline belongs to a sync engine, none of it per-page metadata — and weighed ' + 'wiring a carrier key (option B) and rejected it: that is a feature with a renderer ' + 'behind it, not ledger clean-up. It also weighed tightening the shapes to ' + '`strictObject` and rejected that explicitly — strictness is a property of a PARSE and ' + 'there is no parse, so it would spend a breaking change to leave "a precisely ' - + 'validated dead slot, the more convincing lie" (#4583). Because there was no carrier ' + + 'validated dead slot, the more convincing lie" (the lesson of the datasource capability ' + + 'flags: `readOnly` was precisely validated and read by nothing, while a shipped example ' + + 'called a datasource a read replica and wrote through it). Because there was no carrier ' + 'key there is nothing to tombstone and no `sys_metadata` row or source file for a D2 ' - + 'conversion to rewrite: this entry is the D3 record, the same route 3 as #4834 (kernel ' - + 'plugin-runtime family) and #4938 (`HttpServerConfig`). ⚠️ Not to be confused with ' - + '#5021, which retired the THEME `animation` block — a different file, different defs, ' - + 'and that one did have a carrier key and therefore a tombstone. ADR-0049, #4988.', + + 'conversion to rewrite: this entry is the D3 record, the same route 3 as ' + + '`plugin-runtime-family-retired` (the kernel plugin-runtime family) and the ' + + '`HttpServerConfig` retirement (seven keys no runtime read and no authoring door ' + + 'reached, retired with their container). ⚠️ Not to be confused with the theme-token ' + + 'retirement (theme-driven typography is not a near-term capability, so nine token ' + + 'groups nothing consumed were retired), which retired the THEME `animation` block — a ' + + 'different file, different defs, and that one did have a carrier key and therefore a ' + + 'tombstone. ADR-0049.', acceptanceCriteria: 'No code imports any of the 64 retired names from `@objectstack/spec` or ' + '`@objectstack/spec/ui` — `TouchTargetConfig(Schema)`, `GestureType(Schema)`, ' @@ -4438,7 +4458,9 @@ const step17: MigrationStep = { + 'through a new ADR — carrier key and renderer first, vocabulary second)', reason: 'Both shapes were published `@objectstack/spec/ui` vocabulary with NO AUTHORING DOOR. ' - + '#4001 批 14 measured them three ways on 2026-08-03 and this retirement re-ran all ' + + 'The v17 unknown-key strictness sweep (its ui/ batch 14), which measured each ui/ ' + + 'file for an authoring door before closing any shape, measured them three ways on ' + + '2026-08-03 and this retirement re-ran all ' + 'three against `origin/main` before removing anything, each with a positive control ' + 'that passed in the same run: (1) CARRIER — no schema in `packages/spec/src` declared ' + 'a key of either type (`ui/notification.zod`\'s only non-test importer was the ' @@ -4452,19 +4474,24 @@ const step17: MigrationStep = { + 'itself all resolved `root-graph` in the same run and an injected synthetic carrier ' + 'flipped both; (3) PARSE — zero `.parse()` in objectstack, cloud or objectui outside ' + 'their own unit tests. So nobody could author one and nothing ever validated one: ' - + 'the #3950 shape, an exported schema with no consumer read as a capability, and the ' + + 'the shape of the unwired plugin sandboxing config removed before it, an exported ' + + 'schema with no consumer read as a capability, and the ' + 'ADR-0033 trap where an AI author takes `EmbedConfigSchema` in the published bundle ' + 'as proof the platform serves iframes. Neither is stored metadata and neither has a ' + 'carrier, so no `sys_metadata` row can hold one and there is no source for the D2 ' - + 'chain to rewrite; this entry is the D3 record. 批 14 deliberately did NOT close them ' - + 'with `.strict()` — strictness is a property of a PARSE, and closing a shape nothing ' - + 'parses buys only "a precisely-validated dead slot, the more convincing lie" (#4583) ' - + '— and filed the disposition as #5015, ruled REMOVE on 2026-08-04. Each was orphaned ' - + 'by an earlier retirement one level up: `NotificationAction` lost its wrappers at ' - + '#4610 (`NotificationSchema` / `NotificationConfigSchema`, the #4535 C3 dual-source ' - + 'cleanup — that retirement\'s published "zero consumers" evidence was later falsified ' - + 'for objectui and is corrected on `ui/notification.zod`\'s tombstone; the removal ' - + 'itself stands, #5781), and `EmbedConfig` lost its key at 17.0.0 when the 2026-06 ' + + 'chain to rewrite; this entry is the D3 record. That batch deliberately did NOT close ' + + 'them with `.strict()` — strictness is a property of a PARSE, and closing a shape nothing ' + + 'parses buys only "a precisely-validated dead slot, the more convincing lie" (the lesson ' + + 'of the datasource capability flags, whose `readOnly` was precisely validated and read ' + + 'by nothing) — and left the disposition to ADR-0049\'s enforce-or-remove, which came ' + + 'back REMOVE on 2026-08-04: a dead surface with no authoring door retires ' + + 'implementation-first, as three same-shape rulings that week had already decided. Each ' + + 'was orphaned by an earlier retirement one level up: `NotificationAction` lost its ' + + 'wrappers when the dual-source cleanup removed the `./ui` copies of `NotificationSchema` ' + + '/ `NotificationConfigSchema` (the same names declared differently on other entry ' + + 'points) — that retirement\'s published "zero consumers" evidence was later ' + + 'falsified for objectui, which re-exported both names, and is corrected on ' + + '`ui/notification.zod`\'s tombstone; the removal itself stands — and `EmbedConfig` lost its key at 17.0.0 when the 2026-06 ' + 'liveness audit retired `App.embed` (no iframe route ever read it) — that key still ' + 'stands as a `retiredKey()` tombstone in `app.zod.ts`, so an author who wrote the KEY already ' + 'meets a prescription; this removes the value shape that outlived it. ⚠️ The ' @@ -4474,7 +4501,7 @@ const step17: MigrationStep = { + 'enums. objectui consumed `NotificationActionSchema.shape.variant` as a VOCABULARY ' + '(never a parse) to pin its own hand-written `NotificationActionButton` interface — ' + 'which is exactly why "has a consumer" never meant "has an authoring door" here; that ' - + 'pin is adapted objectui-side when it refreshes this dependency. ADR-0049, #5015.', + + 'pin is adapted objectui-side when it refreshes this dependency. ADR-0049.', acceptanceCriteria: 'No code imports `NotificationActionSchema`, `NotificationAction`, `EmbedConfigSchema` ' + 'or `EmbedConfig` from `@objectstack/spec` or `@objectstack/spec/ui` — both are ' @@ -4501,7 +4528,9 @@ const step17: MigrationStep = { + 'widget is still named the same way it always was: `field.widget` is a plain string ' + 'naming a component the RENDERER has registered, and objectui\'s registry has always ' + 'carried its own runtime manifest for that (`RuntimeWidgetManifest` / ' - + '`RuntimeWidgetSource` in `@object-ui/types`, objectui#3161 / #4115), which models ' + + '`RuntimeWidgetSource` in `@object-ui/types`, renamed off the spec\'s names under ' + + 'objectui\'s rule that a symbol named like a spec export must import it or take a name ' + + 'of its own), which models ' + 'different keys and never derived from these. For localisation: write the ' + 'default-language string on `label` / `description` — the framework generates the ' + 'translation key at registration time from the naming convention — and put ' @@ -4527,22 +4556,27 @@ const step17: MigrationStep = { + 'own unit tests. `NumberFormat` / `DateFormat` DID have a carrier key ' + '(`LocaleConfig.numberFormat` / `.dateFormat`) but the carrier was itself doorless, ' + 'so the subtree was `no door` rather than `no gate` and goes whole — leaving the two ' - + 'leaves behind would strand exported schemas with no consumer (#3950). ' + + 'leaves behind would strand exported schemas with no consumer, which an author reads ' + + 'as a capability. ' + '`I18nObjectSchema` was additionally superseded by its own file-neighbour: ' + '`I18nLabelSchema`\'s documentation already says translation keys are generated at ' + 'registration time and translations live in translation files, and the live ' + 'translation surface is `system/translation.zod.ts`, which uses none of these shapes. ' + 'The 2026-08-06 ruling weighed giving them a carrier (option B) and rejected it: that ' + 'is a feature with a registry and a renderer behind it, not ledger clean-up. ' - + 'Tightening them to `strictObject` was rejected earlier and explicitly (#4001 批 16) ' - + '— strictness is a property of a PARSE and there is no parse, so it would spend a ' + + 'Tightening them to `strictObject` was rejected earlier and explicitly, by the batch of ' + + 'the v17 unknown-key strictness sweep that measured this file as having no authoring ' + + 'door — strictness is a property of a PARSE and there is no parse, so it would spend a ' + 'breaking change to leave "a precisely validated dead slot, the more convincing lie" ' - + '(#4583). With no carrier key there is nothing to tombstone and no `sys_metadata` row ' + + '(the lesson of the datasource capability flags, whose `readOnly` was precisely ' + + 'validated and read by nothing). With no carrier key there is nothing to tombstone and no `sys_metadata` row ' + 'or source file for a D2 conversion to rewrite: this entry is the D3 record, route 3, ' - + 'the same shape as #4988 (the ui/ interaction config family), #4834 (kernel ' - + 'plugin-runtime family) and #4938 (`HttpServerConfig`). ' - + '⚠️ `WidgetManifest.performance`\'s own `retiredKey()` tombstone (#3896 close-out) is ' - + 'SUBSUMED here, the #4657/#4834 way: it goes with the shape that carried it, which is ' + + 'the same shape as `ui-interaction-config-family-retired`, ' + + '`plugin-runtime-family-retired` and the `HttpServerConfig` retirement. ' + + '⚠️ `WidgetManifest.performance`\'s own `retiredKey()` tombstone (left by the ' + + 'close-out sweep that removed the inert `performance` keys no renderer applied) is ' + + 'SUBSUMED here, the way the kernel `activationEvents` tombstone went with the removed ' + + 'plugin-runtime family: it goes with the shape that carried it, which is ' + 'strictly stronger than the tombstone, because there is no longer a manifest to ' + 'author the key INTO. ' + '⚠️ One of the nine widget sites is deliberately NOT retired. ' @@ -4550,12 +4584,13 @@ const step17: MigrationStep = { + 'authorable metadata (it never appeared in `authorable-surface/` or ' + '`json-schema.manifest/` — its `onChange` is a `z.function()`), so "zero parse" is its ' + 'design and not its defect, and it acquired a live cross-repo compile-time consumer ' - + 'one day before 批 16 measured: objectui PR #3289 (2026-08-03) renamed ' + + 'one day before that sweep batch measured: an objectui fix of 2026-08-03, made to follow the ' + + 'spec, renamed ' + '`@object-ui/fields`\' validation slot onto the spec\'s `error` with no alias, the ' + 'form renderer began producing it, and ' + '`packages/fields/src/__tests__/spec-symbol-batch7.test.ts` pins the shape against ' + '`import type { FieldWidgetProps } from \'@objectstack/spec/ui\'` as an intentional ' - + 'tripwire. Re-verified on objectui `origin/main` 2026-08-07. ADR-0049, #5055.', + + 'tripwire. Re-verified on objectui `origin/main` 2026-08-07. ADR-0049.', acceptanceCriteria: 'No code imports `WidgetManifest(Schema|Parsed)`, `WidgetLifecycle(Schema)`, ' + '`WidgetEvent(Schema|Parsed)`, `WidgetProperty(Schema|Parsed)`, ' @@ -4571,8 +4606,9 @@ const step17: MigrationStep = { + 'resolve on `@objectstack/spec/ui` and are asserted to. ⚠️ objectui needs a companion ' + 'PR in the same window: `packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts` ' + 'asserts the spec STILL owns `WidgetManifest` / `WidgetSource` (it is the ' - + '"a workaround should not outlive its reason" half of the objectui#3169 tripwire, ' - + 'designed to go red exactly here), and `packages/types/src/widget.ts`\'s ' + + '"a workaround should not outlive its reason" half of the rename tripwire objectui ' + + 'added when it stopped declaring symbols under names the spec owns, designed to go red ' + + 'exactly here), and `packages/types/src/widget.ts`\'s ' + '"Renamed off the spec\'s `WidgetManifest` name" comments now point at names that no ' + 'longer exist. Both are prescribed responses to this removal, not collateral damage.', }, @@ -11994,8 +12030,11 @@ const step18: MigrationStep = { + 'process — whichever level actually owns the plugin\'s lifetime. The ' + 'monitor reports; it does not act.', reason: - 'ADR-0049 enforce-or-remove, applied one class over from #12428 and ' - + '#12340 in the same host-driven lifecycle library, and for a sharper ' + 'ADR-0049 enforce-or-remove, applied one class over from the two ' + + 'hot-reload retirements in the same host-driven lifecycle library — the ' + + 'file-watching placeholder whose `startWatching` logged success while ' + + 'watching nothing, and the `\'disk\'` / `\'distributed\'` state strategies ' + + 'that fell back to memory in silence — and for a sharper ' + 'reason than either: this key HAD a reader that acted, and what it did ' + 'was not what the key declared. `attemptRestart` called ' + '`plugin.destroy()` and stopped there. The comment above the call read ' @@ -12009,7 +12048,8 @@ const step18: MigrationStep = { + 'plugin was therefore `healthy`, reproduced at ee3595cefd with ' + '`successThreshold: 3` as failed -> recovering (destroyed=1, alive=false) ' + '-> recovering -> recovering -> healthy (destroyed=1, alive=false). ' - + '#11955 made that MORE convincing rather than less, because reaching ' + + 'The fix that made `successThreshold` bind from every status that records ' + + 'a failure made that MORE convincing rather than less, because reaching ' + '`healthy` now costs `successThreshold` CONSECUTIVE passing rounds, so ' + 'the plugin has to earn a declared number of passes to be misreported. ' + 'Meanwhile `restartAttempts` was incremented as though a restart had ' @@ -12024,7 +12064,9 @@ const step18: MigrationStep = { + 'call (positive control: the same scan resolves five real non-test ' + '`plugin.destroy()` call sites, so it sees lifecycle drivers). Building ' + 'that API for a caller that does not exist — no runtime constructs ' - + '`PluginHealthMonitor` (#11825) — is exactly the speculation ADR-0049\'s ' + + '`PluginHealthMonitor`, which is why the maintainer retired its declarative ' + + 'config container on 2026-08-25 and kept the classes as a host-driven ' + + 'library — is exactly the speculation ADR-0049\'s ' + 'staged decision names as the wrong default at this milestone, where the ' + 'shippable liability is the false promise and not the missing feature. ' + 'EXPERIMENTAL requires a roadmap, and a scan of the whole `docs/` ' @@ -12035,10 +12077,13 @@ const step18: MigrationStep = { + 'and "Backoff strategy for restart delays" have nothing left to be the ' + 'vocabulary OF — the same test that took `distributedConfig` out with ' + 'the `stateStrategy` value it was documented as being required for ' - + '(#12340). All three are TOMBSTONED rather than deleted, for #12428\'s ' - + 'reason: a key leaving a SURVIVING def has no route-3 exit, and ' + + '(ruled 2026-08-26: a vocabulary of nothing is not a vocabulary). All ' + + 'three are TOMBSTONED rather than deleted, for the reason the file-watching ' + + 'retirement recorded: a key leaving a SURVIVING def has no route-3 exit, and ' + '`PluginHealthCheckSchema` is not `.strict()`, so a bare deletion would ' - + 'be a silent strip (#3733, ADR-0104) — a milder form of the very defect ' + + 'be a silent strip (ADR-0104; an earlier field-key prune measured exactly ' + + 'that — the parse succeeded and the removed key was dropped without a ' + + 'word) — a milder form of the very defect ' + 'being retired. There is no D2 conversion, because `PluginHealthCheck` ' + 'is not an authorable surface: no metadata-type binding, stack ' + 'collection or manifest embed ever carried it, so there is no authored ' @@ -12055,12 +12100,15 @@ const step18: MigrationStep = { + 'gone with the `restartAttempts` counter, and a plugin that crosses ' + '`failureThreshold` is reported `degraded` / `unhealthy` / `failed` and ' + 'left running. The rest of the monitor is UNCHANGED: registration, ' - + 'periodic checks, the `timeout` race and its refd-timer guard (#4875), ' - + 'the two failure routes sharing counters (#11852), and ' + + 'periodic checks, the `timeout` race and its guard timer (kept ref\'d ' + + 'while the race is undecided, cleared the moment it settles), the two ' + + 'failure routes — a returned failure and a thrown or timed-out check — ' + + 'sharing one failure counter and one threshold comparison, and ' + '`successThreshold` binding from every status that records a failure ' - + '(#11955) all behave exactly as before — `recovering` is now written ' + + 'all behave exactly as before — `recovering` is now written ' + 'only by the success branch, which is the one writer that ever meant it. ' - + 'The #11825 keep still stands: `PluginHealthCheckSchema` still exports ' + + 'The maintainer\'s 2026-08-25 keep of the host-driven library still ' + + 'stands: `PluginHealthCheckSchema` still exports ' + 'from `./kernel` and `PluginHealthMonitor` still exports from ' + '`@objectstack/core` with its tests green.', }, @@ -12086,9 +12134,10 @@ const step18: MigrationStep = { + "plugin auto-discovery (an `oclif` section in the plugin's own `package.json`; see " + '`cli-extension.zod.ts`)', reason: - 'ADR-0049 enforce-or-remove; #10724 (triage graded 2026-08-21, cloud precondition ' - + 'discharged 2026-08-24). #10627 measured, monorepo-wide and non-test with control probes, ' - + 'that the ENTIRE monorepo contains exactly one read of `manifest.contributes` — ' + 'ADR-0049 enforce-or-remove: the nine members retire together, once the cloud half of ' + + 'the census below had come back clean. A census, monorepo-wide and non-test with ' + + 'control probes, measured that the ENTIRE monorepo contains exactly one read of ' + + '`manifest.contributes` — ' + '`packages/objectql/src/engine.ts`, member `kinds` — so all nine members above parsed, ' + 'entered the manifest, and changed nothing. The census stands on three repos: objectstack ' + '(re-verified on current main at claim time), objectui (0 property reads; control: 63 ' @@ -12108,8 +12157,9 @@ const step18: MigrationStep = { + 'present: `os plugin build` runs `ManifestSchema.safeParse` and exits non-zero printing ' + 'the per-key tombstone prescription; TypeScript authors fail earlier still (each key is ' + 'typed `never`). `contributes.kinds` keeps parsing and registering ' - + '(`registry.registerKind`), and `contributes.routes` is untouched pending its own fork ' - + '(#10726). ⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: ' + + '(`registry.registerKind`), and `contributes.routes` is left to its own enforce-or-remove ' + + 'fork (since decided: retired, see `plugin-manifest-contributes-routes-retired`). ' + + '⚠️ Runtime behaviour is deliberately UNCHANGED and must be verified as such: ' + 'nothing ever read the nine members, so removing them removes no behaviour. A package ' + 'ALREADY INSTALLED whose stored manifest carries one degrades to a single ' + '`[metadata_spec_invalid]` log line at registration (the registry\'s `validate()` is a ' @@ -12119,8 +12169,8 @@ const step18: MigrationStep = { { 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)', + 'manifest.contributes.routes (the one member the nine-member retirement deliberately ' + + 'left to its own fork; `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 ' @@ -12128,23 +12178,27 @@ const step18: MigrationStep = { + '`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)', + + 'once the declarative endpoint executor was built and the loud refusal of a ' + + 'non-empty `apis:` became execution)', reason: - 'ADR-0049 enforce-or-remove; #10726, maintainer ruling 2026-08-22 (Option B of the ' + 'ADR-0049 enforce-or-remove; 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 ' + + 'batch carrying the four-axis analysis): remove the key, and redirect every ' + + 'author-facing recommendation of it to the imperative `http.server` mount. A ' + + 'monorepo-wide census with control probes measured zero readers of the key: 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 ' + + 'author-facing corrections landed FIRST (the skill\'s decision table, the ' + + 'dispatcher protocol doc, ADR-0088:40 and app.mdx, each redirected to the ' + + 'imperative mount), and the two remaining teaching sites (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 first: a census of the cloud repository at 5b5925a found 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 ' @@ -12186,9 +12240,9 @@ const step18: MigrationStep = { + '(ADR-0029 D7) injects navigation, and code-level extension lives in the plugin itself ' + '(`init`/`start`)', reason: - 'ADR-0049 enforce-or-remove; #11332 (triage graded 2026-08-23, cloud precondition ' - + 'discharged 2026-08-29 on #12400). #11332 measured, monorepo-wide and non-test with ' - + 'control probes, ZERO reads of each container itself, which settles all eight keys ' + 'ADR-0049 enforce-or-remove, dispatched once the cloud half of the census below came ' + + 'back clean. A census, monorepo-wide and non-test with control probes, measured ZERO ' + + 'reads of each container itself, which settles all eight keys ' + 'beneath them — a key cannot be read if the object holding it never is. The census ' + 'stands on three repos: objectstack (re-verified on current main at claim time; every ' + 'bare `.capabilities` hit classifies to a different surface — driver loader contracts, ' @@ -12235,16 +12289,17 @@ const step18: MigrationStep = { + 'discovery is ever wanted, it gets designed against that registry, not revived ' + 'here', reason: - 'ADR-0049 enforce-or-remove; #11169, maintainer ruling 2026-08-24 (「接受你的建议。」) on ' - + 'the aligned four-facet analysis. The sub-field was declared-but-unenforced on an ' + 'ADR-0049 enforce-or-remove; maintainer ruling 2026-08-24 (「接受你的建议。」) on ' + + 'the aligned four-facet analysis: remove, through the full ADR-0049 ceremony. The sub-field was declared-but-unenforced on an ' + 'authorable published surface: the schema promised that declaring `globs` "enables the ' + 'system to parse and validate new file types" (its own example: a BI plugin handling ' + '`*.report.ts`), and the platform accepted it, stored it, and served it back through ' + '`GET /metadata/kind` — while the discovery the description promised never ran, because ' + 'real glob-driven artifact discovery reads `filePatterns` off the metadata type registry ' + 'and `metadata-plugin.zod.ts` records outright that `contributes.kinds` does not extend ' - + 'it. Measured in PR #11168 and re-verified at claim time with the card\'s positive ' - + 'control: zero value reads anywhere (the only non-test occurrences of the path are the ' + + 'it. Measured by the engine-lane fix that made kind registration log its declared ' + + '`id` (which also found the `kind` bucket itself reachable through `GET /metadata/:type`), ' + + 'and re-verified at claim time with a positive control: zero value reads anywhere (the only non-test occurrences of the path are the ' + 'schema declaration and two type positions), and no in-repo manifest authors the key ' + 'outside test fixtures. Enforce was weighed and rejected on all four facets: it would ' + 'build a SECOND discovery channel parallel to `filePatterns` for a spelling with zero ' @@ -12286,9 +12341,11 @@ const step18: MigrationStep = { + 'advertise diligence keeps the surviving securityContact and vulnerabilityDisclosure ' + 'blocks, which are contact terms rather than a verdict.', reason: - 'ADR-0049 enforce-or-remove; maintainer ruling 2026-09-07 on #15932 (director seat, decision batch #65, adopted verbatim 「同意」). ' - + 'This is the second half of the scanner retirement — issue 14919, a number since ' - + 'DELETED from the board, landed as PR #15930. That card retired PluginSecurityScanner — a ' + 'ADR-0049 enforce-or-remove; maintainer ruling 2026-09-07 (adopted verbatim 「同意」): ' + + 'retire the scan-result family and its securityScan sibling, because once the scanner ' + + 'was gone nothing so much as imported their types. ' + + 'This is the second half of the scanner retirement recorded as ' + + 'plugin-security-scanner-retired. That retirement removed PluginSecurityScanner — a ' + '@objectstack/core class that shipped as a SECURITY control and could not fail, whose ' + 'verdict was status "passed" for every plugin it was ever handed. The SCHEMAS the ' + 'scanner fed survived it, and the scanner had been their only importer of any kind (a ' @@ -12321,10 +12378,12 @@ const step18: MigrationStep = { + 'recorded as checked. Its two siblings were MEASURED rather than assumed, and the ' + 'record is corrected here: both were ALREADY GONE when that ruling was written. The ' + 'incident "malware" type was a member of system/IncidentCategory, and the whole ' - + 'incident-response family was retired by #15513 (maintainer ruling 2026-09-05 — two ' - + 'days BEFORE the 2026-09-07 ruling that made it conditional); see ' + + 'incident-response family was retired whole, with the training and change-management ' + + 'families (maintainer ruling 2026-09-05: not roadmapped, so retired rather than marked ' + + 'experimental — two days BEFORE the 2026-09-07 ruling that made it conditional); see ' + 'incident-response-family-retired. And marketplace-admin.zod.ts was deleted outright ' - + 'with the cloud subpath (#16526); see cloud-subpath-retired. Verified on this tree by ' + + 'with the cloud subpath (ruled 2026-09-07: cloud does not re-host the control-plane ' + + 'files it never consumed); see cloud-subpath-retired. Verified on this tree by ' + 'shape: both files return zero tree entries and no *.zod.ts names malware at all, ' + 'against a lit control where "scanning" still returns a live declaration in ' + 'marketplace.zod.ts. So the conditional question is ONE enum member wide, not three, ' @@ -12332,8 +12391,7 @@ const step18: MigrationStep = { + '⚠️ The out-of-repo consumer population is NOT MEASURED. @objectstack/spec is published, ' + 'so this removal is breaking for consumers no download, dependent or source telemetry ' + 'was consulted for — accepted as an input to the ruling, exactly as that retirement states ' - + 'of its own three exports, and not a reason to soften the removal. #15932, PR #15930 ' - + '(for the deleted issue 14919), ADR-0049, ADR-0087.', + + 'of its own three exports, and not a reason to soften the removal. ADR-0049, ADR-0087.', acceptanceCriteria: 'No source imports KernelSecurityScanResult, KernelSecurityVulnerability or either ' + 'Schema from @objectstack/spec/kernel: both defs are absent from the built kernel ' @@ -12373,8 +12431,8 @@ const step18: MigrationStep = { + 'the GitHub Advisory Database, OSV — and treat an unaudited third-party plugin as ' + 'untrusted code.', reason: - 'ADR-0049 enforce-or-remove; maintainer ruling 2026-09-05 on #14919 (director summon #14, ' - + 'decision batch #42, ruled A: retire in three surfaces). The class shipped on ' + 'ADR-0049 enforce-or-remove; maintainer ruling 2026-09-05: retire the class and its two ' + + 'companion types with no replacement export, rather than repair it. The class shipped on ' + '`@objectstack/core`\'s public barrel as a SECURITY control and could not fail. `scan()` ' + 'composed five private scanners: four of them (`scanCode`, `scanMalware`, `scanLicenses`, ' + '`scanConfiguration`) allocated an empty issue array, logged and returned it with no code ' @@ -12394,8 +12452,10 @@ const step18: MigrationStep = { + 'per-instance Map discarded with the object), and hence no seam `applyConversionsToStored' + 'Item` would ever reach. The enforced channel is tsc, at the consumer\'s own import site; ' + 'for anyone it does not reach, this ledger entry and the generated upgrade guide are the ' - + 'only channel there is. That is the `contracts.IDataDriver.findStream` (#4484) and ' - + '`actor-user-roles-to-positions` (#6011) disposition — a TS/API contract, no stored ' + + 'only channel there is. That is the disposition of `contracts.IDataDriver.findStream` ' + + '(removed with no tombstone, because nothing parses a driver object) and of ' + + '`actor-user-roles-to-positions` (the `ctx.user` `roles` alias, closed at once on the ' + + 'maintainer\'s word rather than given a window) — a TS/API contract, no stored ' + 'source, no tombstone, tsc at the call site — applied to a surface one layer further out ' + 'than either: those are declared in `packages/spec`, this one only in `packages/core`. ' + '⚠️ The out-of-repo consumer population is NOT MEASURED. Zero constructors were found in ' @@ -14455,10 +14515,15 @@ const step18: MigrationStep = { + 'not renamed. Node-level keys (`visibleWhen`, `id`, `style`, …) stay on the component ' + 'node, where the page runtime reads them.', reason: - 'These were two more instances of the #8691/#8744 class: console-registered widgets on ' + 'These were two more instances of the class already closed for `record:reference_rail` ' + + 'and then for `record:alert` / `record:quick_actions` / `record:history`, each by ' + + 'declaring a strict `ComponentPropsMap` row measured from the renderer\'s read points: ' + + 'console-registered widgets on ' + '`@objectstack/cloud-connection`\'s published Setup pages, reachable through the ' + 'component type union\'s open string arm, with registered renderers but no ' - + '`ComponentPropsMap` row — so the #5068 props gate\'s dispatch skipped them as ' + + '`ComponentPropsMap` row — so the props gate\'s dispatch (it parses `properties` ' + + 'against the type\'s row at publish and lint time, and skips a type with no row because ' + + 'the type union is open) skipped them as ' + 'unregistered and any authored key rode through every validator in silence. The new ' + 'rows are strict and EMPTY, measured from the renderers\' actual read points at the ' + 'objectui pin (not from the registrations\' declared-input lists): both registrations ' @@ -14482,18 +14547,22 @@ const step18: MigrationStep = { replacement: 'a positive-integer bound (>= 1), or no declaration at all ("no minimum" is ' + 'expressed by OMITTING `minLength`, never by `minLength: 0`). The row-level key is a ' + 'per-form override that can only NARROW what the referenced object field already ' - + 'declares (the object field surface tightened first: #11566/#11949) — so a malformed ' + + 'declares (the object field surface tightened first, to a positive integer, by ' + + 'maintainer rulings) — so a malformed ' + 'row value is deleted, and a bound that was actually wanted is re-declared as a ' + 'positive integer, or dropped in favour of the object field\'s own authoritative ' + 'declaration', reason: - '#12174: the form-field row carried the pre-#11566 shape — bare `z.number()` — after ' - + 'the object-field surface converged (`maxLength` #11566, `minLength` #11949, both ' + 'The form-field row still carried the object field\'s old shape — bare `z.number()` — ' + + 'after that surface converged (`maxLength` by the maintainer\'s 2026-08-24 ruling, ' + + '`minLength` by the 2026-08-25 one, which refused zero too: both ' + '`z.number().int().min(1)`). The row keys are LIVE, measured in objectui: the spec ' - + 'bridge (`packages/react/src/spec-bridge/bridges/form-view.ts` mapField, ' - + 'objectui#5898) and plugin-form (`sectionFields.ts` normalizeSectionField) both copy ' + + 'bridge (`packages/react/src/spec-bridge/bridges/form-view.ts` mapField, which maps ' + + 'every spec key or explains why it does not, so none is dropped in silence) and ' + + 'plugin-form (`sectionFields.ts` normalizeSectionField) both copy ' + 'them onto the runtime field, the console FormPage merges `override.maxLength ?? ' - + 'def.maxLength` onto the rendered input (objectui#5595), and the fields package ' + + 'def.maxLength` onto the rendered input (fixed so that a form\'s own bound wins over ' + + 'the object\'s, as its docstring promised), and the fields package ' + 'builds react-hook-form validation rules from `minLength`/`maxLength` — so ' + '`maxLength: 0` on a form row reached the DOM as an input that accepts nothing, and ' + 'the public-form resolve route (`GET /forms/:slug`) serves the rows verbatim to ' @@ -14519,14 +14588,16 @@ const step18: MigrationStep = { + 'non-integer or negative values (`scale: 2.5`, `precision: -1`)', replacement: 'a non-negative integer digit count, or no declaration at all. The row-level ' + 'key is a per-form override of the referenced object field\'s own declaration (that ' - + 'surface tightened first: #8321) — a malformed row value is deleted, and a count ' + + 'surface tightened first, to a non-negative integer) — a malformed row value is ' + + 'deleted, and a count ' + 'that was actually wanted is re-declared as a non-negative integer (`scale: 2.5` was ' + 'probably `2` or `3`)', reason: - '#12174: the form-field row carried the pre-#8321 shape — bare `z.number()` — after ' - + 'the object-field surface converged on `z.number().int().min(0)` for both digit ' + 'The form-field row still carried the object field\'s old shape — bare `z.number()` — ' + + 'after that surface converged on `z.number().int().min(0)` for both digit ' + 'counts. The row keys are LIVE, measured in objectui: the spec bridge ' - + '(`form-view.ts` mapField, objectui#5898) and plugin-form (`sectionFields.ts`) copy ' + + '(`form-view.ts` mapField, which maps every spec key or explains why it does not) and ' + + 'plugin-form (`sectionFields.ts`) copy ' + 'them onto the runtime field, `ObjectForm` derives the number input\'s step from ' + '`precision`, and the `NumberField` widget reads `scale` — so a malformed count ' + 'flowed into rendering arithmetic (`Math.pow(10, -precision)`) with no defined ' @@ -14556,8 +14627,9 @@ const step18: MigrationStep = { + 'is mechanical: a feature-flag gate and a record-state gate answer different ' + 'questions, so the author chooses which surface the gate belongs on.', reason: - 'objectstack#12665, ruled 2026-08-27 on objectui#6262 (option B — vocabulary ' - + 'narrowing): one authored form view is served on two kinds of route, and a ' + 'Ruled by the maintainer on 2026-08-27 (option B — vocabulary narrowing: a form view may ' + + 'not name `features.*` in a predicate, and the authoring door refuses it loudly): one ' + + 'authored form view is served on two kinds of route, and a ' + '`features.*` predicate got two verdicts from the same text. Inside an app ' + '(`/apps/:appName/*`) the root resolves against the real auth-config flags; on the ' + 'standalone form routes (`/forms/:name`, public `/f/:slug`) no app context exists, ' @@ -14595,7 +14667,9 @@ const step18: MigrationStep = { + 'the offending spelling verbatim, so the whitespace an author cannot see in an editor is ' + 'visible in the message, next to the name to write instead.', reason: - '#17499. All three keys were a bare `z.string()`, so a padded group-by name was valid ' + 'The same padded-name defect the grouping-level narrowing ' + + '(`ui-list-view-grouping-field-padded-refused`) refused, on the axis that one scoped out ' + + 'by name, and given the same refusal. All three keys were a bare `z.string()`, so a padded group-by name was valid ' + 'authored metadata all the way to the renderers. The name is a LOOKUP KEY on every row, ' + 'measured in objectui at `dda8f3815`: the kanban board resolves its lane as ' + '`laneField = groupByField || groupField || detectStatusField(objectDef)` and buckets cards ' @@ -14647,7 +14721,7 @@ const step18: MigrationStep = { + '`\'business_unit\'`. The refusal names the offending spelling verbatim, so the ' + 'whitespace an author cannot see in an editor is visible in the message.', reason: - '#17360, ruling C on objectui#7347 (maintainer 「其他同意」, decision batch #110 item 5): ' + 'Ruled by the maintainer on 2026-09-10 (「其他同意」): ' + 'refuse at the producer. `field` was a bare `z.string()`, so a padded grouping level ' + 'was valid authored metadata all the way to the renderers. Measured on objectui ' + '(M1-M11 with live controls): the projection harvester `collectGroupingFieldRefs` ' @@ -14697,13 +14771,18 @@ const step18: MigrationStep = { + 'not renamed. Node-level keys (`visibleWhen`, `id`, `style`, …) stay on the component ' + 'node, where the page runtime reads them.', reason: - 'This was a third instance of the #8691/#8744 class (#11575 closed the previous two): a ' + 'This was a third instance of the class closed for the `record:*` blocks by declaring a ' + + 'strict `ComponentPropsMap` row measured from the renderer\'s read points (the strict, ' + + 'empty `cloud-connection:panel` / `marketplace:installed-list` rows closed the previous ' + + 'two): a ' + 'console-registered widget on `@objectstack/mcp`\'s plugin-shipped Setup page, ' + 'reachable through the component type union\'s open string arm, with a registered ' - + 'renderer but no `ComponentPropsMap` row — so the #5068 props gate\'s dispatch ' + + 'renderer but no `ComponentPropsMap` row — so the props gate\'s dispatch (it parses ' + + '`properties` against the type\'s row, and skips a type with no row because the type ' + + 'union is open) ' + 'skipped it as unregistered, any authored key rode through every validator in ' - + 'silence, and door 3 of the mcp canonical-envelope gate (#12269) had to carry a ' - + 'standing exemption for the type. The new row is strict and EMPTY, measured from the ' + + 'silence, and door 3 of the canonical-envelope gate `@objectstack/mcp` was given for its ' + + 'shipped page had to carry a standing exemption for the type. The new row is strict and EMPTY, measured from the ' + 'renderer\'s actual read points at the objectui pin (not from the registration\'s ' + 'declared-input list): the registration ignores the component node entirely, so the ' + 'widget accepts no configuration at all, and an authored key is now a publish-time ' @@ -14729,18 +14808,20 @@ const step18: MigrationStep = { + 'authored to mean "no paging" is `showPagination: false` with no `pagination` bag, ' + "since the bag's PRESENCE is what enables paging)", reason: - '#19046: this door carried the pre-#7751 read-point shape — `pagination: z.unknown()` ' + 'This door still carried the shape it was given when the `object-*` blocks first got ' + + '`ComponentPropsMap` rows measured from their read points — `pagination: z.unknown()` ' + 'and `pageSize: z.number()` — after the view arm converged on ' + '`z.number().int().positive()`. So the SAME authored member carried two accept sets ' + 'and renderers read the looser one: `PaginationConfigSchema` (`view.zod.ts`) refuses ' + '`pageSize: 0` and pins that refusal by name, and every other `pageSize` the package ' + 'declares is bounded with its own throwing pin (`kernel/metadata-plugin.zod.ts`, ' + '`marketplace/marketplace.zod.ts`) — the component arm was the only one that ' - + 'accepted `0`. The value is LIVE: measured at objectui#9853, an authored ' + + 'accepted `0`. The value is LIVE: an objectui grid measurement found that an authored ' + '`pagination.pageSize: 0` reached `ObjectGrid`, went out on the wire as `$top: 0` ' + 'and rendered ZERO ROWS, with no grouping needed to trigger it, and it reached the ' - + 'renderer through this arm. objectui#9896 repaired the consumer half (a resolver at ' - + 'every read point, fail-soft, one loud diagnostic); this is the declaration half, ' + + 'renderer through this arm. objectui\'s grid plugin repaired the consumer half — it ' + + 'now refuses a non-positive page size at all three read points (one resolver, ' + + 'fail-soft, one loud diagnostic); this is the declaration half, ' + 'and it is not a prerequisite for that repair. ' + '⚠️ The `pagination` bag itself stays OPEN (`z.looseObject`): only the two members ' + 'whose value is a page size are bounded, and sibling keys parse and pass through ' @@ -14758,7 +14839,8 @@ const step18: MigrationStep = { { id: 'ui-react-list-view-binding-aliases-retired', surface: '`kind:\'react\'` page source — `` and `` ' - + '(the react-tier overlay aliases #11284 had published as deprecated)', + + '(the react-tier overlay aliases published as deprecated when the react tier ' + + 'converged on the metadata-tier vocabulary)', replacement: '`` — ListViewSchema\'s own ' + '`data` data source and `type` view kind, the same two keys a metadata list view authors. ' + '`objectName="x"` → `data={{ provider: \'object\', object: \'x\' }}`; `viewType="kanban"` → ' @@ -14768,11 +14850,12 @@ const step18: MigrationStep = { 'A react page\'s source is a JSX string, not a keyed document: `objectstack migrate meta` ' + 'rewrites stored metadata by key and cannot rewrite props inside authored source, so the ' + 'move is by hand. The contract deprecated both aliases in favour of the metadata-tier ' - + 'spelling (#11284) while objectui\'s ListView still read only `objectName`, so the canonical ' + + 'spelling (maintainer ruling 2026-08-23: the react tier converges on the metadata-tier ' + + 'vocabulary, deprecating first) while objectui\'s ListView still read only `objectName`, so the canonical ' + 'spelling validated green and rendered an empty list. The consumer fold has landed (objectui ' + '`normalizeListViewSchema`, console pin a472b071: `data.provider === \'object\'` → ' + '`objectName`, and the author\'s `type` read for the view kind), and the maintainer ruled the ' - + 'aliases retired with no deprecation window (#14791, 2026-09-07). Writing either alias is now ' + + 'aliases retired with no deprecation window (2026-09-07). Writing either alias is now ' + 'a publish-time `react-prop-retired` error carrying this prescription — never a silent pass ' + 'on a key the renderer happens to still read.', acceptanceCriteria: @@ -14802,8 +14885,10 @@ const step18: MigrationStep = { + 'declared until the renderer reads the contract spelling; `visibleWhen` / `visibility` ' + 'on the alert → `visible`; a locale map as history text → a literal string)', reason: - 'These were the four `record:*` components the #4001/#5068 gate could not reach after ' - + '#8691 closed the rail: each had a registered objectui renderer (and, bar ' + 'These were the four `record:*` components the component-props unknown-key gate (an ' + + 'authorable surface refuses a key it does not declare; for a component\'s `properties`, ' + + 'by parsing them against the type\'s `ComponentPropsMap` row) could not reach after ' + + 'the rail was given its strict row: each had a registered objectui renderer (and, bar ' + '`record:discussion`, a `PageComponentType` entry and a console palette slot) but no ' + '`ComponentPropsMap` row, so the props gate\'s dispatch skipped them as unregistered and ' + 'every authored key rode through. A typo\'d `severty` on the platform\'s own banner ' @@ -14837,7 +14922,9 @@ const step18: MigrationStep = { + '`related` → `entries`; `object` → `objectName`; `label` → `title`; a `title` locale map ' + '→ a literal string, or omit it to keep the localized object label)', reason: - 'The rail was the `record:*` component the #4001/#5068 gate could not reach: it had a ' + 'The rail was the `record:*` component the component-props unknown-key gate (an ' + + 'authorable surface refuses a key it does not declare; for a component\'s `properties`, ' + + 'by parsing them against the type\'s `ComponentPropsMap` row) could not reach: it had a ' + 'registered renderer and a `PageComponentType` entry but no `ComponentPropsMap` row, so ' + 'the props gate\'s dispatch skipped it as unregistered and every authored key rode ' + 'through. Measured on 17.0.0 GA end to end: a planted entry `filter` passed tsc, '