From 2a78a0f65938756ae05d85f321051c371ec17258 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 02:11:28 +0000 Subject: [PATCH] docs(spec): correct #4610's falsified "zero importers" evidence and its uncompilable FROM to TO, forward-note the stale rc.2 areas caveat (#5781, #5809) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Text-only. No schema byte, export, conversion or baseline moves; the #4610 and #4651 retirements are not reopened. #5781 — two published claims about the Notification retirement were false: 1. "zero importers in all three repos". objectui#3310 measured both `./ui` names alive at 17.0.0-rc.1: `packages/types/src/index.ts` re-exported them with `export ... from '@objectstack/spec/ui'`, and `packages/core/src/protocols/NotificationProtocol.ts` consumed them via the `@object-ui/types` barrel in two public signatures. The three-repo scan matched `import ... from` statement text and saw neither hop. 2. The FROM `./ui` to TO `./api` migration line does not compile: `./api`'s `Notification` is the REST inbox row, disjoint from the removed toast shape. Rewritten to "no replacement"; counted honestly while rewriting it, three `./ui` enums survive, not four (#5015 took `NotificationAction`). A methodology sentence lands beside the tombstone: a cross-repo liveness verdict is read off the resolved SYMBOL GRAPH, covering `export ... from` re-exports and barrel-indirect consumption. Third miss of that class, after #4667 / #4709. #5809 — the shipped `## 17.0.0-rc.2` section still asserted "the server does not walk `areas`", which #4722 falsified and the same file's `## 17.0.0-rc.4` entry already refutes. Forward-note added in place. Editorial class rule applied (PM ruling on #5809): shipped CHANGELOG sections are never rewritten, only forward-noted; still-unconsumed `.changeset/*.md` are GA inputs and are corrected directly; live source prose is corrected directly. `docs/protocol-upgrade-guide.md` and `spec-changes.json` are regenerated by `gen:upgrade-guide` / `gen:spec-changes`, never hand-edited. `content/docs/releases/` is untouched. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018ffcE95NaMJcL9XJ9VDYgk --- .changeset/notification-dual-source-c3.md | 51 +++++++--- ...ification-retirement-evidence-corrected.md | 74 ++++++++++++++ docs/protocol-upgrade-guide.md | 2 +- packages/spec/CHANGELOG.md | 19 ++++ packages/spec/spec-changes.json | 4 +- .../spec/src/migrations/migrations.test.ts | 40 ++++++++ packages/spec/src/migrations/registry.ts | 10 +- .../ui/notification-embed-retirement.test.ts | 4 +- packages/spec/src/ui/notification.test.ts | 96 +++++++++++++++++++ packages/spec/src/ui/notification.zod.ts | 56 ++++++++--- 10 files changed, 325 insertions(+), 31 deletions(-) create mode 100644 .changeset/notification-retirement-evidence-corrected.md diff --git a/.changeset/notification-dual-source-c3.md b/.changeset/notification-dual-source-c3.md index 696c1b4282..2beb515da2 100644 --- a/.changeset/notification-dual-source-c3.md +++ b/.changeset/notification-dual-source-c3.md @@ -10,36 +10,63 @@ each resolved to **two different declarations** depending on the import path — the #4411 dual-source trap. Resolution (three-repo, import-statement-level consumer scan: framework, cloud, objectui): +> **Correction (#5781): the `./ui` declarations were NOT consumer-free — the +> removal stands, the evidence sentence does not.** The scan above matched +> `import … from` statement text, and objectui reached these two names by two +> hops it could not see: `packages/types/src/index.ts` re-exported both with +> `export … from '@objectstack/spec/ui'`, and +> `packages/core/src/protocols/NotificationProtocol.ts` consumed them via the +> `@object-ui/types` barrel in the public signatures of +> `resolveNotificationConfig` / `specNotificationToToast` (objectui#3310, at +> 17.0.0-rc.1). objectui does not ask for the retirement back: that bridge had +> zero in-repo callers, `@object-ui/react`'s locally-declared +> `NotificationSystemConfig` is what runs, and objectui deleted the bridge to +> follow this retirement. A cross-repo liveness verdict must be read off the +> resolved SYMBOL GRAPH — covering `export … from` re-exports and +> barrel-indirect consumption — never off import-statement text; this was the +> third miss of that class, after #4667 / #4709 (`app.homePageId`). + - **Removed** `NotificationSchema` / `Notification` from `@objectstack/spec/ui`. This was a toast/banner "notification instance" shape (`type`/`severity`/`message`/`duration`/`actions`/`position` + ARIA - props) with **zero importers** in all three repos — objectui's toaster - never adopted it. The live contract is `./api`'s `Notification(Schema)`: + props) that objectui's toaster never adopted — its only holders were the + re-export bridge named in the correction above, which objectui has since + deleted. The live contract is `./api`'s `Notification(Schema)`: the REST inbox row (`id`/`type`/`title`/`body`/`read`/`data`/`actionUrl`/ `createdAt`) embedded in `ListNotificationsResponseSchema`, served by `/api/v1/notifications`, implemented by `@objectstack/client`, and mirrored by `InboxNotification` in `@objectstack/spec/contracts` (ADR-0030: the bell reads this shape). - FROM `import { NotificationSchema, type Notification } from '@objectstack/spec/ui'` → - TO `import { NotificationSchema, type Notification } from '@objectstack/spec/api'`. - **Shape change**: the api row is an inbox record, not a presentation - config — the ui shape's `severity` / `duration` / `dismissible` / - `actions` / `position` / ARIA fields do not exist there. For the + TO: **no replacement.** Do NOT re-point this import at + `@objectstack/spec/api` — that is the same name under a different + contract, and following it does not compile. The api `Notification` is the + REST inbox row (`id` / `type` / `title` / `body` / `read` / `data` / + `actionUrl` / `createdAt`); the removed ui shape was a toast instance + (`message` / `severity` / `position` / `duration` / `dismissible` / + `actions` + ARIA). The two share zero fields, and aliasing one to the + other would re-create the dual-source trap this change closed. For the presentation vocabulary keep using the ui enums, which are unchanged: - `NotificationTypeSchema`, `NotificationSeveritySchema`, - `NotificationPositionSchema`, `NotificationActionSchema` (+ their - types) still live in `@objectstack/spec/ui`. + `NotificationTypeSchema`, `NotificationSeveritySchema` and + `NotificationPositionSchema` (+ their types) still live in + `@objectstack/spec/ui`. Declare the instance shape locally, as objectui + does. (`NotificationActionSchema` was listed here too at the time of this + change; #5015 retired it in 17.0.0-rc.3, so three enums survive, not + four.) - **Removed** `NotificationConfigSchema` / `NotificationConfig` from **both** `@objectstack/spec/system` and `@objectstack/spec/ui` — the bare name left - the spec export surface entirely. Both declarations had zero importers in - all three repos and were wired into no parent schema. The system side (a + the spec export surface entirely. Neither declaration was wired into any + parent schema; the `./system` one had no importers in any of the three + repos, and the `./ui` one was held only by the objectui re-export bridge + named in the correction above. The system side (a channel + template + recipients + schedule + retryPolicy + tracking "unified notification management protocol") predates ADR-0030's accepted delivery architecture and advertised capability the runtime does not deliver (its channel enum's `push`/`slack`/`teams`/`webhook` dead-letter, #3197; nothing reads `schedule`/`retryPolicy`/`tracking`). The ui side (a toaster global config: `defaultPosition`/`defaultDuration`/`maxVisible`/ - `stackDirection`/`pauseOnHover`) was never adopted by objectui. + `stackDirection`/`pauseOnHover`) was never adopted by objectui's toaster, + which reads its own `NotificationSystemConfig` instead. - FROM `import { NotificationConfigSchema } from '@objectstack/spec/system'` (or `.../ui`) → TO: no direct replacement. The live delivery vocabulary is `NotificationService.emit` (`INotificationService`, diff --git a/.changeset/notification-retirement-evidence-corrected.md b/.changeset/notification-retirement-evidence-corrected.md new file mode 100644 index 0000000000..27c77f8b4f --- /dev/null +++ b/.changeset/notification-retirement-evidence-corrected.md @@ -0,0 +1,74 @@ +--- +'@objectstack/spec': patch +--- + +docs(spec): correct two published claims about the #4610 Notification retirement, and forward-note the stale `areas` caveat in the shipped rc.2 changelog (#5781, #5809) + +**What changes is what the repo SAYS, not what it does.** No schema byte, no +export, no conversion and no baseline moves; `@objectstack/spec/ui` still does +not publish `Notification(Schema)` / `NotificationConfig(Schema)` and +`@objectstack/spec/system` still does not publish `NotificationConfig(Schema)`. +Two of the three corrections are to live contract prose, one is a forward-note +on a shipped changelog section, and one unconsumed changeset (a v17 GA release +input) is corrected at the source. + +**1. "zero importers in all three repos" was false for objectui (#5781).** #4610 +justified removing the two `./ui` notification wrappers with a three-repo, +import-statement-level consumer scan. objectui#3310 measured the same names at +17.0.0-rc.1 and found both alive: `packages/types/src/index.ts` re-exported them +with `export … from '@objectstack/spec/ui'`, and +`packages/core/src/protocols/NotificationProtocol.ts` consumed them through the +`@object-ui/types` barrel, in the public signatures of `resolveNotificationConfig` +/ `specNotificationToToast`. The scan matched `import … from` statement text and +could see neither hop. + +⛔ **The retirement is not reopened.** objectui does not ask for it back: the +`@object-ui/core` bridge had zero in-repo callers, the implementation that runs +is `@object-ui/react`'s locally-declared `NotificationSystemConfig`, and objectui +deleted the bridge to FOLLOW the retirement rather than re-declare vocabulary the +spec had just dropped. Only the sentence that justified the removal changes. + +**2. The FROM → TO would have sent an author to code that does not compile +(#5781).** #4610 published `from '@objectstack/spec/ui'` → `from +'@objectstack/spec/api'` for `Notification(Schema)`. `./api`'s `Notification` is +the REST inbox row (`id` / `type` / `title` / `body` / `read` / `data` / +`actionUrl` / `createdAt`); the removed `./ui` shape was a toast instance +(`message` / `severity` / `position` / `duration` / `dismissible` / `actions` + +ARIA). They share zero fields — same name, a different contract, which is the +dual-source trap #4610 closed rather than a new home for the old shape. The +guidance is now **no replacement**: keep `./ui`'s presentation enums and declare +the instance shape locally, as objectui does. Counted honestly while rewriting +it: **three** enums survive (`NotificationType` / `NotificationSeverity` / +`NotificationPosition`), not the four #4610 listed — #5015 retired +`NotificationAction` at 17.0.0-rc.3. + +**3. The methodology, written down beside the tombstone.** A cross-repo liveness +verdict must be read off the RESOLVED SYMBOL GRAPH, covering at minimum +`export … from` re-exports and consumption that reaches the spec indirectly +through a downstream barrel package — never off import-statement text. This was +the third miss of that class, after #4667 / #4709 (`app.homePageId`). + +**4. Shipped changelog sections get forward-notes, never rewrites (#5809).** A +changelog's value is in being a faithful record of what was shipped and said, so +the stale sentences stay and a clearly-marked correction is added inside the +section, pointing at where the corrected statement lives. Three notes land in +`## 17.0.0-rc.2`: two on the #4610 entry (`0a936ea`, the evidence and the +FROM → TO), and one on the #4651 app-area entry (`ad047d2`), whose caveat *"per-item +gating inside an area is enforced by the shell only, because the server does not +walk `areas`"* has not held since #4722 — `filterAppForUser` runs the same +`filterNav` over every `areas[].navigation`. The same file's `## 17.0.0-rc.4` +entry `e4c8b6c` (#5337 / PR #5796) already carries the corrected statement, so +until now one `CHANGELOG.md` asserted a prescription in one section and refuted +it in another; a reader arriving by keyword search could be sent off to do an +unnecessary navigation-tree refactor. + +Corrected directly (live contract text and GA inputs, not shipped records): +`packages/spec/src/ui/notification.zod.ts`'s tombstone, +`packages/spec/src/migrations/registry.ts`'s protocol-17 `#5015` rationale (which +projects verbatim into `docs/protocol-upgrade-guide.md` and `spec-changes.json` +via `gen:upgrade-guide` / `gen:spec-changes` — both regenerated, never hand-edited), +and the still-unconsumed `.changeset/notification-dual-source-c3.md`, which is a +legal input to the v17 GA release notes while the repo is in changesets pre mode. +New pin tests keep both false premises from returning: the registry `reason` is +pinned in `migrations.test.ts`, the tombstone's four verdicts in +`notification.test.ts`. ⛔ `content/docs/releases/` is untouched. diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index f1ea8e88f9..cc3e3a6d4f 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -349,7 +349,7 @@ Last, it reconciles the SDUI component-props surface with the renderers that ser - 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. - 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`, deleted for zero consumers), 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. #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. - 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`. - **`hook-context-session-roles-retired`** — `data.hookContext.session.roles` → (removed — gate on `session.userId` / `session.isSystem`; for PRIVILEGE ask the security service, which reads `permissions` / `positions` / posture off the execution context, ADR-0095 D3) - Why not automatic: Declared on the runtime hook context, read by exactly two consumers, produced by nobody. The two readers were the approvals record lock and the delegation write guard, each opening with `session.roles?.includes('admin')`; ObjectQL's `buildSession()` builds the session field by field and has never written `roles`, and nothing else feeds a HookContext in objectstack, cloud or objectui (cloud's hook consumers read `hookContext?.session?.userId`; objectui's `roles` are the `/auth/me` user payload, a different surface; an ACTION body's `ctx.session` is a different untyped object that does carry `roles`, tracked apart and unaffected). Both branches were therefore dead on every real engine path — an authorization decision in shape only, and a second admin dialect competing with the one ADR-0090 D3 / ADR-0095 D3 sanction. #4839 (PR #5049) removed the readers; this removes the declaration, per ADR-0049 enforce-or-remove. This is a RUNTIME context, not stored metadata: the engine builds a HookContext per operation and nothing persists one, so no `sys_metadata` row, example or template can carry the key and there is no source for the D2 chain to rewrite — the `openApi31` (#4579) / `activationEvents` (#4657) shape, one semantic TODO rather than a stack conversion. The key IS tombstoned (`HookContextSchema` is deliberately not `.strict()` — a plain delete would strip it silently, #3733 / ADR-0104), so a consumer that parses a context it was handed still meets the prescription. ADR-0049, #5050. diff --git a/packages/spec/CHANGELOG.md b/packages/spec/CHANGELOG.md index 87fceb9a39..711f4c77ab 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -5776,6 +5776,12 @@ logic, conditions }`) into `filter`, where the spec declares `ViewFilterRule[]` top-level `navigation` tree, or in its own app. Trading one false belief for a weaker one would have repeated the defect this removal exists to end. + > **Correction (#5809):** this caveat has not held since #4722, which landed in + > the same 17.0.0 window — `filterAppForUser` runs the same `filterNav` over + > every `areas[].navigation`, so an item gate inside an area is server-enforced + > and needs no navigation restructuring. See the `17.0.0-rc.4` entry `e4c8b6c` + > (#5337) higher up in this file. + - 7d21581: feat(spec)!: retire the six remaining `authorWarn` dead keys — book/group `translations`, `job.id`, `translation.validationMessages`, `app.homePageId`, `app.areas[].order` (#4667) The #4488 liveness audit marked as `authorWarn` the keys whose _declaration_ @@ -7183,6 +7189,12 @@ stack?, code? }`). Neither side had any consumer outside spec; the path — the #4411 dual-source trap. Resolution (three-repo, import-statement-level consumer scan: framework, cloud, objectui): + > **Correction (#5781):** this statement-level scan could not see objectui's + > `export … from` re-export nor its `@object-ui/types` barrel hop, so the + > "zero importers" claims below are false for the two `./ui` declarations + > (objectui#3310). The removal itself stands; the corrected tombstone is in + > `packages/spec/src/ui/notification.zod.ts`. + - **Removed** `NotificationSchema` / `Notification` from `@objectstack/spec/ui`. This was a toast/banner "notification instance" shape (`type`/`severity`/`message`/`duration`/`actions`/`position` + ARIA @@ -7202,6 +7214,13 @@ stack?, code? }`). Neither side had any consumer outside spec; the `NotificationTypeSchema`, `NotificationSeveritySchema`, `NotificationPositionSchema`, `NotificationActionSchema` (+ their types) still live in `@objectstack/spec/ui`. + + > **Correction (#5781):** there is no replacement — following this FROM → TO + > does not compile. `./api`'s `Notification` is the REST inbox row and shares + > zero fields with the removed toast shape; keep the `./ui` presentation + > enums and declare the instance shape locally. (`NotificationActionSchema` + > left too, at 17.0.0-rc.3 / #5015 — three enums survive, not four.) + - **Removed** `NotificationConfigSchema` / `NotificationConfig` from **both** `@objectstack/spec/system` and `@objectstack/spec/ui` — the bare name left the spec export surface entirely. Both declarations had zero importers in diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index 159be5a8eb..5dadce7c9e 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -608,7 +608,7 @@ "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`, deleted for zero consumers), 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. #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." }, { "surface": "data.hookContext.session.roles", @@ -1318,7 +1318,7 @@ "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`, deleted for zero consumers), 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. #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." }, { "surface": "data.hookContext.session.roles", diff --git a/packages/spec/src/migrations/migrations.test.ts b/packages/spec/src/migrations/migrations.test.ts index aa6e9580d0..167ff1c7ef 100644 --- a/packages/spec/src/migrations/migrations.test.ts +++ b/packages/spec/src/migrations/migrations.test.ts @@ -114,6 +114,46 @@ describe('migration chain (ADR-0087 D3)', () => { }); }); + // Same class as the block above, one field over. A `semantic` entry's `reason` + // is projected verbatim into `docs/protocol-upgrade-guide.md` and + // `spec-changes.json` (ADR-0087 D4), so a falsified premise recorded here is + // PUBLISHED advice — not a code comment. This entry (#5015) explained its two + // orphans by pointing one level up at #4610, and repeated #4610's stated + // evidence: the `./ui` notification wrappers were "deleted for zero consumers". + // objectui#3310 disproved that at 17.0.0-rc.1 — `packages/types/src/index.ts` + // re-exported both names with `export … from '@objectstack/spec/ui'` and + // `NotificationProtocol.ts` consumed them through the `@object-ui/types` + // barrel, two hops an import-statement-level scan cannot see (the third miss + // of that class, after #4667 / #4709). The RETIREMENT is untouched; only the + // sentence that justified it moves. + describe('protocol-17 #5015 entry — stops republishing #4610\'s falsified evidence (#5781)', () => { + const entry = () => + MIGRATIONS_BY_MAJOR[17]!.semantic.find( + (s) => s.id === 'ui-notification-action-embed-config-retired', + ); + + it('finds the entry, and it still explains the #4610 orphaning (anti-vacuity)', () => { + expect(entry()).toBeDefined(); + expect(entry()!.reason).toMatch(/#4610/); + expect(entry()!.reason).toMatch(/NotificationConfigSchema/); + }); + + it('never asserts the wrappers were deleted for having zero consumers', () => { + // Pinned on the ASSERTION, not on the words: the entry may still name the + // claim in order to correct it — going quiet about it would leave a reader + // who remembers the old guide believing the old reason. + expect(entry()!.reason).not.toMatch(/(deleted|removed) for (having )?zero consumers/i); + }); + + it('names the correction and keeps the removal itself standing', () => { + const r = entry()!.reason; + expect(r).toMatch(/falsified/); + expect(r).toMatch(/#5781/); + // ⛔ A correction to the evidence is not an un-retirement. + expect(r).toMatch(/removal itself stands/); + }); + }); + describe('composition (cross-major is the designed-for case)', () => { it('composes only the steps in (from, to]', () => { const chain = composeMigrationChain(10, 11); diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index b396bba5a7..4ca31504c4 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -2018,10 +2018,12 @@ const step17: MigrationStep = { + '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`, deleted for zero ' - + 'consumers), 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 ' + + '#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 ' diff --git a/packages/spec/src/ui/notification-embed-retirement.test.ts b/packages/spec/src/ui/notification-embed-retirement.test.ts index e8dca90ad7..a79ba7e3fd 100644 --- a/packages/spec/src/ui/notification-embed-retirement.test.ts +++ b/packages/spec/src/ui/notification-embed-retirement.test.ts @@ -16,7 +16,9 @@ import { describe, it, expect } from 'vitest'; // // The two arrived at orphanhood by the same route, one level apart: // - `NotificationAction` lost its wrappers at #4610 (`NotificationSchema` / -// `NotificationConfigSchema`, deleted for zero consumers); +// `NotificationConfigSchema`, the #4535 C3 dual-source cleanup — its "zero +// consumers" evidence was later falsified for objectui and is corrected on +// `notification.zod`'s tombstone; the removal itself stands, #5781); // - `EmbedConfig` lost its key at 17.0.0 (`App.embed`, retired by the 2026-06 // liveness audit and still standing as a `retiredKey()` tombstone in // `app.zod.ts` — the value shape simply outlived the key). diff --git a/packages/spec/src/ui/notification.test.ts b/packages/spec/src/ui/notification.test.ts index aa492cea0c..240ed80e2b 100644 --- a/packages/spec/src/ui/notification.test.ts +++ b/packages/spec/src/ui/notification.test.ts @@ -1,3 +1,7 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; + import { describe, it, expect } from 'vitest'; import { NotificationTypeSchema, @@ -83,3 +87,95 @@ describe('Type exports', () => { expect(hasNotificationConfigSchema).toBe(false); }); }); + +// ─── [#5781] The #4610 tombstone states the CORRECTED evidence ────────────── +// +// The names are gone, so #4610 left no runtime message to pin — the tombstone +// is the only place a reader learns WHY they went, and the reason it gave was +// false. objectui#3310 measured it at 17.0.0-rc.1: `packages/types/src/index.ts` +// re-exported both `./ui` names with `export … from '@objectstack/spec/ui'`, and +// `packages/core/src/protocols/NotificationProtocol.ts` consumed them through +// the `@object-ui/types` barrel in two public signatures. An +// import-statement-level scan sees neither hop — the third miss of that class, +// after #4667 / #4709 (`app.homePageId`). +// +// The pin is on the VERDICTS, never on the wording, for the reason #5337's +// rationale pin records: a tombstone that simply goes quiet about a claim leaves +// the reader holding it, so naming the old sentence in order to refute it is +// required, and only the tense-and-assertion may be pinned. Four verdicts: +// the evidence is corrected, the retirement is NOT reopened, `./api` is NOT a +// migration target, and the methodology lesson is written down where the next +// cross-repo liveness verdict gets made. +describe('[#5781] ./ui notification tombstone — corrected evidence, no false FROM → TO', () => { + const SOURCE = path.resolve( + path.dirname(url.fileURLToPath(import.meta.url)), + 'notification.zod.ts', + ); + + /** + * The `#4610` note as PROSE: comment markers stripped and whitespace + * collapsed, so an assertion cannot pass or fail on where a line happens to + * wrap. Read out of the source rather than a generated artifact — the source + * is what a reader of the module sees. + */ + const tombstone = (() => { + const source = fs.readFileSync(SOURCE, 'utf8'); + const start = source.indexOf('// [#4610]'); + return source + .slice(start) + .split('\n') + .map((line) => line.replace(/^\s*\/\/ ?/, '')) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); + })(); + + it('finds the #4610 note at all (anti-vacuity)', () => { + expect(tombstone).toMatch(/^\[#4610\]/); + expect(tombstone).toMatch(/NotificationConfigSchema/); + expect(tombstone.length).toBeGreaterThan(800); + }); + + it('does not assert the ./ui wrappers had zero consumers', () => { + // The exact sentence #4610 shipped, in the present-tense assertive form + // objectui#3310 disproved. The note may still QUOTE the claim (and does) — + // what it may not do is state it as fact. + expect(tombstone).not.toMatch(/wrappers had ZERO consumers/i); + expect(tombstone).not.toMatch(/equally consumer-free/i); + }); + + it('records the measured objectui consumers and both hops that hid them', () => { + expect(tombstone).toMatch(/That was false for objectui/); + expect(tombstone).toMatch(/objectui#3310/); + expect(tombstone).toMatch(/export … from/); + expect(tombstone).toMatch(/@object-ui\/types/); + expect(tombstone).toMatch(/NotificationProtocol\.ts/); + }); + + it('⛔ does not read as reopening the retirement', () => { + // Correcting the evidence is not an un-retirement. objectui followed the + // removal (it deleted the bridge); a note that only said "the reason was + // wrong" would invite the next reader to put the names back. + expect(tombstone).toMatch(/retirement STANDS/); + expect(tombstone).toMatch(/does not ask for the retirement back/); + expect(tombstone).toMatch(/deleted the bridge to FOLLOW this retirement/); + }); + + it('refuses `./api` as the migration target, with the reason a reader can check', () => { + // #4610 published FROM `./ui` → TO `./api`. It does not compile: same name, + // disjoint contract. Pinning the VERDICT, not the absence of the arrow — + // the note quotes the arrow in order to withdraw it. + expect(tombstone).toMatch(/NO migration target/); + expect(tombstone).toMatch(/does not compile/); + expect(tombstone).toMatch(/shares ZERO fields/); + // The survivors, counted — `NotificationAction` left at #5015, so a reader + // told "the four ui enums" would go looking for one that is not there. + expect(tombstone).toMatch(/three enums, not four/); + }); + + it('writes down the methodology so the fourth miss is not owed to it', () => { + expect(tombstone).toMatch(/RESOLVED SYMBOL GRAPH/); + expect(tombstone).toMatch(/barrel package/); + expect(tombstone).toMatch(/#4667 \/ #4709/); + }); +}); diff --git a/packages/spec/src/ui/notification.zod.ts b/packages/spec/src/ui/notification.zod.ts index fa61cbc318..859328886f 100644 --- a/packages/spec/src/ui/notification.zod.ts +++ b/packages/spec/src/ui/notification.zod.ts @@ -58,8 +58,9 @@ export type NotificationPosition = z.input; // notification-actions key; the barrel was this module's only non-test // importer. The `./ui` "notification instance" and "notification system // config" wrappers that could have carried it were themselves deleted in -// #4610 for having zero consumers (see the note below), which is what left -// this shape an orphan. +// #4610 (see the corrected note below — that retirement's "zero consumers" +// evidence did not hold, the removal did), which is what left this shape +// an orphan. // 2. **Graph reachability** — BFS from the 24 metadata-type roots plus // `defineStack`'s `ObjectStackSchema` (the closure `build-schemas.ts` uses // for the #4650 deletion check, including its derived-clone bridge) never @@ -92,12 +93,45 @@ export type NotificationPosition = z.input; // [#4610] `NotificationSchema` / `Notification` and `NotificationConfigSchema` // / `NotificationConfig` were removed from this module (dual-source cleanup, -// #4535 C3). The `./ui` "notification instance" and "notification system -// config" wrappers had ZERO consumers across framework, cloud and objectui — -// only the presentation vocabulary above (type/severity/position/action) is -// consumed (objectui pins its toaster implementation against it). The bare -// name `Notification(Schema)` now belongs to `@objectstack/spec/api` alone: -// the REST inbox-row contract served by `/api/v1/notifications` (ADR-0030's -// bell reads that shape). `NotificationConfig(Schema)` left the export -// surface entirely — its `./system` twin was equally consumer-free and -// contradicted ADR-0030's accepted delivery model. +// #4535 C3). The bare name `Notification(Schema)` now belongs to +// `@objectstack/spec/api` alone: the REST inbox-row contract served by +// `/api/v1/notifications` (ADR-0030's bell reads that shape). +// `NotificationConfig(Schema)` left the export surface entirely — its +// `./system` twin was wired into no parent schema and contradicted ADR-0030's +// accepted delivery model. What survives on this module is the presentation +// vocabulary above: `NotificationType` / `NotificationSeverity` / +// `NotificationPosition` — three enums, not four, since #5015 took +// `NotificationAction` — which objectui's toaster reads as a vocabulary. +// +// ⚠️ [#5781] The retirement STANDS; the evidence #4610 published for it does +// not. #4610 recorded these two `./ui` wrappers as having ZERO consumers +// across framework, cloud and objectui. That was false for objectui: at +// 17.0.0-rc.1 its `packages/types/src/index.ts` re-exported both names with +// `export … from '@objectstack/spec/ui'`, and +// `packages/core/src/protocols/NotificationProtocol.ts` consumed them through +// the `@object-ui/types` barrel, in the public signatures of +// `resolveNotificationConfig` / `specNotificationToToast` (objectui#3310). +// objectui does not ask for the retirement back, and this note does not +// reopen it: that bridge had zero in-repo callers, the implementation that +// actually runs is `@object-ui/react`'s own locally-declared +// `NotificationSystemConfig`, and objectui deleted the bridge to FOLLOW this +// retirement rather than re-declare vocabulary the spec had just dropped. +// +// ⚠️ [#5781] There is NO migration target for the removed `./ui` +// `Notification`. #4610 published a FROM `'@objectstack/spec/ui'` → TO +// `'@objectstack/spec/api'` line; an author who follows it does not compile. +// `./api`'s `Notification` is the REST inbox row (`id` / `type` / `title` / +// `body` / `read` / `data` / `actionUrl` / `createdAt`) and shares ZERO fields +// with the removed toast shape (`message` / `severity` / `position` / +// `duration` / `dismissible` / `actions`) — same name, a different contract, +// which is the dual-source trap #4610 closed, not a new home for the old +// shape. Keep the three presentation enums above for the vocabulary and +// declare the instance shape locally, as objectui does. +// +// Methodology, so a fourth repetition is not owed to the same blind spot: a +// cross-repo liveness verdict must be read off the RESOLVED SYMBOL GRAPH, not +// off `import … from` statement text — at minimum it has to cover +// `export … from` re-exports and consumption that reaches the spec indirectly +// through a downstream barrel package. Statement-level scanning missed exactly +// those two hops here, after missing the same class at #4667 / #4709 +// (`app.homePageId`).