Skip to content

Commit 63085c3

Browse files
feat(service-messaging,spec,cli)!: retire sys_notification_subscription — no reader ever expanded a subscription (ADR-0030 L3, amended) (#25024)
Closes #24923 Clause-②: yes (narrowing: `sys_notification_subscription` retires, carried into 18.x by its ADR-0087 semantic entry under 「甲」) **Tier H.** This diff touches `docs/adr/0030-notification-platform-convergence.md`, so it stays a draft until an authorized approval (and it owes a `CONTRACT_REVIEW_TIER` contract review). No AI seat lands it. ## What and why The maintainer's ruling D on the parent card (decision record 6290342354, recorded 6293376907) retires `sys_notification_subscription`. ADR-0030 described it as Layer-3 audience expansion, and that expansion was never wired: - every producer passes an explicit audience (`EmitInput.audience` is required); - no audience member asks for subscribers, and no recipient-resolver branch reads a topic's subscriptions; - so the Setup "Notification Subscriptions" grid let an administrator author rows that made nobody receive anything. The ruling supersedes #9807's keep-with-a-comment decision. The table stays in deployed databases, on the `scim-provider-object-retired` / `sys-view-definition-retired` precedent. Under standing rule 「甲」 (#6015, record 6292005854) the ADR-0087 semantic entry carries the narrowing, so it ships in 18.x as a `minor` with the BREAKING banner. ## Re-measured on current main (`e4d604efd3`) before editing | reading | `sys_notification_subscription` | control `sys_notification_preference` | |:--|--:|--:| | files under `packages/**/src`, tests and generated bundles excluded | 6 | 8 | | of those, readers of the rows | **0** | 2 (`preference-resolver.ts`, `fan-out-system-context.ts`) | | repo-wide files | 44 | 55 | | objectui at the console pin `cf153eb2e5` | **0** | 2 files | The 6 files are the object's own declaration, the plugin's registration and nav item, a translation-bundle comment, `PLATFORM_OBJECTS_BY_PACKAGE`, the `object.zod.ts` doc comment, and the CLI ownership inventory. None of them reads a row. The cloud repository is out of reach from this session, so its readers are **NOT MEASURED**. The entry's reason says this repo and the Studio repo only. ## What changed - **service-messaging:** the object file and its organization-unique pin test are deleted. The object is gone from the manifest registration, from boot provisioning, from the Setup nav (`nav_notification_subscriptions`), from both export barrels and from the i18n extract config. The generated object bundles were regenerated with `node scripts/check-i18n-bundles.mjs --write`: 178 lines deleted, nothing added. The sibling tests drop the object. `plugin-declares-event-object.test.ts` gains an absence pin: the object is not registered and has no nav item. The same capture sees the surviving preference item, which is the vacuity control. - **platform-objects:** the Setup nav label `nav_notification_subscriptions` is removed from the hand-written app translations (`en`, `zh-CN`, `ja-JP`, `es-ES`), along with its three recorded source hashes. This is outside the claim's listed file surface. It is the same retirement's translation half: the nav item it labelled no longer exists. - **spec:** - the name leaves `PLATFORM_OBJECTS_BY_PACKAGE['service-messaging']`, pinned in `platform-object-names.test.ts` with the preference as control; - the `object.zod.ts` `system-data` doc comment no longer names it; - new semantic entry `18.sys-notification-subscription-retired.ts`, in the precedents' shape: no `RETIRED_KEYS_BY_MAJOR` entry and no D2 conversion, because no authorable spec key moves; - one `STEP18_RATIONALE` fragment, inserted at its sorted id position. - **cli:** `organization-ownership-inventory.ts` keeps a row for the retired-but-present table, now census `platform-census-2026-08-31` only. It moves from fate 3 `attribution` to fate 4 `report` with `notNullExempt`, citing the ADR-0087 entry, on the `sys_scim_provider` precedent (bef7bdc). It is not `retired-before-census`, because the first census counted this object. - New unit pin: a `RETIRED_AFTER_FIRST_CENSUS` population, held to the same premises as `RETIRED_BEFORE_CENSUS`. - New integration pin in `organization-ownership-plan.integration.test.ts`: the last declared table shape, under `isolated` and `single`, plans as fate 4. The NULL row is reported, never attributed, and `notNull.willReceive` is false. Control: without the row, the plan refuses with `uninventoried-platform-table`. - **ADR-0030:** a dated `Amended (2026-10-20 …)` entry on the status line, in the same form as the existing 2026-09-20 amendment. The L3 row is revised to preference-only, and the remaining-work "Subscription-driven fan-out" bullet is struck through and marked **Withdrawn**. - **QA checklist:** a resolution line in `FOLLOW-UPS.md` §9d, which is append-never-rewrite, for the §9c row. - **Census:** `scripts/platform-object-tenancy-census.json` regenerated, 85 → 84 registered. - **Changeset:** `.changeset/24923-sys-notification-subscription-retired.md`. `minor` for service-messaging, spec and cli, `patch` for platform-objects, with the BREAKING banner, ADR-0087 disposition `registered sys-notification-subscription-retired` and the FROM → TO mapping. `content/docs/releases/` is untouched. **Dogfood upgrade-harness fixtures: no edit, deliberately.** `fixture-18.0` and `fixture-18.0.0` are dumps of databases written by released code, and both hold the table with 0 rows. A retired-but-present table is exactly what they should hold. Regenerating them from the same refs would reproduce the same bytes. The two precedents predate the fixtures, so there is no fixture edit to mirror. The harness was run instead (below): HEAD boots over the fixture, and the ceremony plans and applies with the new fate-4 row. ## Verification (head `356172a1f9`; the package suites ran on the pre-merge tree, and `check:generated` was re-run after the merge) - `pnpm --filter @objectstack/service-messaging test`: `Test Files 53 passed (53)`, `Tests 589 passed (589)`. `typecheck` (both tsconfigs): exit 0. - `pnpm --filter @objectstack/spec test`: `Test Files 703 passed (703)`, `Tests 21365 passed | 1 todo`. `typecheck`: exit 0. - `pnpm --filter @objectstack/spec check:generated`: `All 14 generated artifacts are up to date` (re-run after the `origin/main` merge, on a rebuilt spec). - CLI (`--project unit` in full exceeds the foreground cap, so this run is narrowed, as declared): every `organization-ownership*` test plus `test/migrate-meta-engine-guidance.test.ts`, which prints every semantic entry and pins it free of tracker numbers. `Test Files 25 passed | 1 skipped (26)`, `Tests 285 passed`. `pnpm --filter @objectstack/cli typecheck`: exit 0. The rest of the unit and integration tiers is left to CI. - Dogfood `test/upgrade-18x-harness.dogfood.test.ts`: `Tests 12 passed (12)`. - `@objectstack/platform-objects` test and typecheck: **NOT MEASURED**. Three attempts at the shared verify lock queued 9 minutes each without acquiring it (the holder was another worktree's long chain), and a fourth was cut by the foreground cap. The diff there is 7 deleted lines in the hand-written app translation dictionaries. `check:i18n`, which reads them, is green. CI owns the suite. - Gates from `node scripts/pm/dispatch-gates.mjs --commands`: 101 commands, 99 exit 0. Among them: `check-adr-0087-registration --base origin/main` (`registered sys-notification-subscription-retired (new here …)`), `check:i18n` (`all bundles in sync`), `check:platform-object-tenancy-census`, `check:spec-changes`, `check:upgrade-guide`, `check:doc-authoring`, `check:nul-bytes`, `check:cross-package-test-inputs`, `check:platform-checklist`, `check:changeset-no-major` and `check:pm-governed-merges`. - `check:i18n-coverage` first hit its build prerequisite. After the closure build it was re-run: `OK (14 config(s), 621 baselined untranslated string(s), none new)`. - `check:dual-build-cjs-loads`: **NOT MEASURED**. Its prerequisite needs `dist/` for packages this diff does not touch (studio, client-react, connector-slack, …). CI owns it. ## Acceptance notes - `organization-ownership`'s `inventoryDigest` changes with the row. A plan file written by an earlier CLI is refused with `plan-digest-mismatch`, as for any inventory change, and the changeset says to re-plan. - Under `single`, NULL-organization subscription rows were previously planned to the Default Organization (fate 3). They are now listed and left alone (fate 4), because nothing reads them. - Open PR overlap was checked before opening: #24970 also edits `registry.ts.template`, in a different sorted gap. #24984 edits platform-objects' generated object bundles, which are different files from the hand-written app translations edited here. - Historical mentions stay as written: the superseded ADR-0012 draft, ADR-0103's list, `docs/design/` and `docs/handoff/`. #24770 is not addressed here beyond its subscription half: the parent remains open for execution card ②, the template narrowing. ## 维护者速读(草稿) **改了什么:** 下线 Setup 里的「通知订阅」:对象、菜单项、导出与翻译都移除,平台对象名单去掉这个名字;数据库里的旧表原样保留不动。ADR-0030 的 L3 行与剩余工作清单按裁决 D 记一笔带日期的修订。 **为什么改:** 订阅从来没有接上线:所有发通知的地方都写明了收件人,没有任何代码读订阅表,管理员建了订阅也不会有人收到通知。你在 #24770 裁了 D:退役订阅(取代 #9807「保留并加注释」)。 **风险与代价(含回滚):** 引用了这个对象名的元数据会被提示「未知对象」;代码里 import `NotificationSubscription` 会编译失败(changeset 写明了改法)。旧表与旧数据不删,升级仪式把它列为「只报告、不归属」。回滚就是 revert 本 PR,对象与菜单即恢复,数据从未动过。 **席位意见:** **你要做的:** 审阅 ADR-0030 的修订文字;同意则批准本 PR(Tier H,需你的批准才能落地)。 --- _Generated by [Claude Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6621836 commit 63085c3

41 files changed

Lines changed: 264 additions & 544 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/service-messaging': minor
3+
'@objectstack/spec': minor
4+
'@objectstack/cli': minor
5+
'@objectstack/platform-objects': patch
6+
---
7+
8+
feat(service-messaging,spec,cli)!: `sys_notification_subscription` retires — no reader ever expanded a subscription into recipients (ADR-0030 L3, amended)
9+
10+
Clause-②: yes (narrowing)
11+
12+
<!-- adr-0087: registered sys-notification-subscription-retired -->
13+
14+
**BREAKING**, shipped as `minor` under the repo's launch-window convention for breaking changes. It goes into 18.x because its ADR-0087 semantic entry, `sys-notification-subscription-retired`, carries the narrowing.
15+
16+
ADR-0030 described `sys_notification_subscription` as Layer-3 audience expansion: "who is subscribed to a topic". That expansion was never wired. Every producer names its recipients explicitly (`EmitInput.audience` is required: a flow notification node's recipients, an approval's approvers, a mention's users), no audience member asks for subscribers, and nothing reads the table's rows. So an administrator could author subscriptions in Setup that made nobody receive anything. The maintainer ruled to retire the object rather than build the expansion. The ruling supersedes the earlier decision to keep it with a code comment saying it was not wired, and ADR-0030 is amended to match.
17+
18+
**What moves for consumers, FROM → TO.**
19+
20+
- **The object.** FROM `import { NotificationSubscription } from '@objectstack/service-messaging'` TO nothing: delete the import. `MessagingServicePlugin` neither registers nor provisions the object any more, so the generic data door no longer serves it.
21+
- **Setup navigation.** FROM a Notification Subscriptions item (`nav_notification_subscriptions`) in the Setup app's Configuration group TO no item. Notification Preferences and Notification Templates stay. The item's labels leave the platform app translations, and the object's keys leave service-messaging's generated object bundles.
22+
- **The name.** `PLATFORM_OBJECTS_BY_PACKAGE['service-messaging']` (`@objectstack/spec/system`) lists six objects, and `isPlatformProvidedObjectName` answers `false` for `sys_notification_subscription`. A stack that names it (a lookup target, a flow trigger, a permission entry, a navigation item) is now flagged as naming an unknown object. Fix: remove the reference. To control who gets a notification, name the audience on the producer. To mute a topic for a user, write a `sys_notification_preference` row, which is unchanged.
23+
- **`os migrate organization-ownership` (`@objectstack/cli`).** The inventory row for `sys_notification_subscription` moves FROM fate 3 (`attribution`: organization-less rows went to the Default Organization under `single`) TO fate 4 (`report`: rows listed by id, never attributed, never deleted). The table is exempt from NOT NULL, because no declaration anchors tenancy on its column any more. The row stays, so a database that still holds the table plans instead of being refused as carrying an uninventoried platform table. The plan's `inventoryDigest` changes: a plan file written by an earlier CLI no longer matches, and `--apply --plan-file` refuses it with `plan-digest-mismatch`. Re-run `os migrate organization-ownership --out PLAN`, review it, and apply that.
24+
25+
**Existing databases.** The table is left in place untouched: no backfill, no reaper, no migrate command. Schema sync is additive and never drops a table, so a database an earlier release provisioned keeps `sys_notification_subscription` and any rows an administrator wrote, and nothing reads them. Export any row worth keeping. Dropping the table is the operator's call, by hand: `DROP TABLE sys_notification_subscription`.

‎docs/adr/0030-notification-platform-convergence.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0030 — Notification Platform Convergence (single ingress, layered pipeline)
22

3-
**Status**: Accepted (2026-06-01) — **P0–P3b2 shipped** (P3b-2 digest collapse landed); cross-repo objectui cut-over remains. See [§ Implementation status & remaining work](#implementation-status--remaining-work). · **Amended** (2026-09-20, #16194 — P0's idempotent data migration `migrateSysNotificationToEvent` is **retired**: it had zero production callers and no way to be run, and both ways of giving it one — an `os migrate` sub-command and a boot-time invoker — were refused. Pre-ADR-0030 `sys_notification` rows are therefore **not** carried by the platform on this line. ⛔ Nothing else moves: single ingress, the layered object model and the remaining objectui cut-over read exactly as accepted.)
3+
**Status**: Accepted (2026-06-01) — **P0–P3b2 shipped** (P3b-2 digest collapse landed); cross-repo objectui cut-over remains. See [§ Implementation status & remaining work](#implementation-status--remaining-work). · **Amended** (2026-09-20, #16194 — P0's idempotent data migration `migrateSysNotificationToEvent` is **retired**: it had zero production callers and no way to be run, and both ways of giving it one — an `os migrate` sub-command and a boot-time invoker — were refused. Pre-ADR-0030 `sys_notification` rows are therefore **not** carried by the platform on this line. ⛔ Nothing else moves: single ingress, the layered object model and the remaining objectui cut-over read exactly as accepted.) · **Amended** (2026-10-20, maintainer ruling D on #24770, recorded 6293376907 — the L3 `sys_notification_subscription` object is **retired**: the subscription→recipient expansion was never wired, every producer names an explicit audience (`EmitInput.audience` is required), and the Setup grid let an administrator author subscriptions that made nobody receive anything. The ruling supersedes #9807's keep-with-a-comment decision. The object, its registration, its Setup nav item and its translations leave; existing tables are left in place untouched (ADR-0087 semantic entry `sys-notification-subscription-retired`). L3 is now preference-only; a topic-subscription capability, if real demand appears, returns as a new decision with a who-may-subscribe-whom permission model. ⛔ Nothing else moves.)
44
**Supersedes / refines**: [ADR-0012 — Notification Platform](./0012-notification-platform.md) (Draft)
55
**Related**: [ADR-0019 — Approval as a Flow Node](./0019-approval-as-flow-node.md), [ADR-0022 — Connectors vs Messaging Channels](./0022-connectors-vs-messaging-channels.md)
66
**Build spec**: [docs/design/notification-platform-convergence.md](../design/notification-platform-convergence.md)
@@ -28,7 +28,7 @@ Do **not** merge the two tables into one. **Un-conflate** them back into the ADR
2828
| Layer | Object | Role |
2929
|---|---|---|
3030
| L2 Event | `sys_notification` *(re-modeled to event)* | one row per `emit`: `topic`, `payload`(json), `severity`, **`dedup_key`** (idempotency), `source_object/id`, `actor_id`, `created_at`. **No recipient, no read-state.** |
31-
| L3 Resolve + Preference | `sys_notification_subscription`, `sys_notification_preference` | audience expansion (`role:` / `owner_of:record` / `team:` / explicit ids; **email→id resolved here**), user×topic×channel toggles, quiet-hours, digest; mandatory topics bypass |
31+
| L3 Resolve + Preference | `sys_notification_preference` *(`sys_notification_subscription` retired 2026-10-20 — see the status line)* | audience resolution of the producer's explicit audience (`role:` / `owner_of:record` / `team:` / explicit ids; **email→id resolved here**), user×topic×channel toggles, quiet-hours, digest; mandatory topics bypass. ~~Topic-subscription audience expansion~~ — never wired, retired with its object. |
3232
| L4 Delivery (outbox) | `sys_notification_delivery` | one row per (event × recipient × channel); state machine `pending→sent→failed/dead`; retry/backoff/dedup; the durable spine |
3333
| L5 Materialize + Receipt | `sys_inbox_message` (in-app), `sys_email`, `sys_user_device` (push), connector dispatch (webhook/Slack); `sys_notification_receipt` | each channel renders delivery into a consumable artifact; **the bell reads `sys_inbox_message`**; read/clicked/dismissed live in `sys_notification_receipt` |
3434
| Cross-cutting | `sys_notification_template` (topic×channel×locale) | rendering, with generic fallback to `title`/`body` |
@@ -119,9 +119,12 @@ configurable daily send-hour (windows flush at local midnight / Monday 00:00).
119119
`connector_action` Slack path already works today).
120120
- **`defineTopic()`** declarative topic catalog (Studio discoverability for topics /
121121
templates / preferences) — the low-code backbone.
122-
- **Subscription-driven fan-out**: expand a topic's `sys_notification_subscription`
122+
- ~~**Subscription-driven fan-out**: expand a topic's `sys_notification_subscription`
123123
principals when a producer emits without an explicit audience (object exists;
124-
expansion not wired).
124+
expansion not wired).~~ **Withdrawn (2026-10-20, ruling D on #24770)** — the object
125+
is retired instead (status line): zero pull, every producer names its audience,
126+
and the expansion would need a permission model nobody asked for. Existing
127+
tables are left in place untouched, read by nothing.
125128
- **MJML** compilation for email (P3a treats `mjml` format as raw HTML).
126129
- Quiet-hours **tz fallback** to a `sys_user` timezone field (currently
127130
`quiet_hours.tz` → UTC).

‎docs/qa/platform-checklist/FOLLOW-UPS.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,11 @@ resolve-at-load only).
515515
**11-ledger** universe) and integration-system.datasource-admin-lifecycle (same
516516
"unledgered" claim, plus its "always-available static catalog" wording predating the
517517
#9391/#9593 uniform auth floor).
518+
- **§9c's `sys_notification_subscription` Setup grid row is RESOLVED** — retired by
519+
#24923 (maintainer ruling D on #24770): the object, its Setup nav item and its
520+
translations left service-messaging, its name left the platform-object registry, and
521+
the table stays in place untouched (ADR-0087 semantic entry
522+
`sys-notification-subscription-retired`; ADR-0030 amended 2026-10-20).
518523

519524
### 9e. Blocked-item re-audit
520525

‎packages/cli/src/utils/organization-ownership-inventory.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ const CENSUS_2026_08_31_EXAMPLES = [
9292
*/
9393
const RETIRED_BEFORE_CENSUS = { sys_scim_provider: 'scim-provider-object-retired' } as const;
9494

95+
/**
96+
* Platform objects the first census counted that have since retired by an
97+
* ADR-0087 entry leaving their table in place untouched: the registry no
98+
* longer names them, yet an upgraded database still holds the table. Frozen,
99+
* keyed to the entry each row's citation names.
100+
*/
101+
const RETIRED_AFTER_FIRST_CENSUS = { sys_notification_subscription: 'sys-notification-subscription-retired' } as const;
102+
95103
/**
96104
* #14570 and #15086, read in: each population's table has its own fate, citing
97105
* the record that read it in (a runtime string carries no tracker number).
@@ -143,6 +151,24 @@ describe('ADR-0131 D10 inventory — every censused object has one fate and a ci
143151
).toEqual([...retired].sort());
144152
});
145153

154+
it('counted by the first census, retired since with the table left in place: fate 4, citing its ADR-0087 entry', () => {
155+
const counted = [...CENSUS_2026_08_31_PLATFORM_WITH_COLUMN, ...CENSUS_2026_08_31_PLATFORM_WITHOUT_COLUMN] as readonly string[];
156+
const registered = new Set(Object.values(PLATFORM_OBJECTS_BY_PACKAGE).flat());
157+
for (const [object, adr0087Entry] of Object.entries(RETIRED_AFTER_FIRST_CENSUS)) {
158+
// The population's premise: the first census counted it and the tree no longer declares it.
159+
expect(counted, object).toContain(object);
160+
expect(registered.has(object), object).toBe(false);
161+
// The entry the citation names exists, and its ruling is what licenses fate 4: the table stays untouched.
162+
const semantic = Object.values(MIGRATIONS_BY_MAJOR).flatMap((step) => step.semantic).find((s) => s.id === adr0087Entry);
163+
expect(semantic?.acceptanceCriteria, adr0087Entry).toContain('left in place untouched');
164+
const entry = inventoryEntryFor(object)!;
165+
expect(entry, object).toMatchObject({ census: ['platform-census-2026-08-31'], fate: 'report' });
166+
expect(entry.citation, object).toContain(adr0087Entry);
167+
expect(entry.citation, object).not.toMatch(/#\d/);
168+
expect(entry.notNullExempt, object).toBeTruthy();
169+
}
170+
});
171+
146172
it('the cloud supplement: listed as cloud-carried, fate 4, never a guessed fate', () => {
147173
expect(CLOUD_PROVIDED_OBJECT_NAMES.length).toBeGreaterThan(0);
148174
expect(unlisted(CLOUD_PROVIDED_OBJECT_NAMES)).toEqual([]);

‎packages/cli/src/utils/organization-ownership-inventory.ts‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
*
1818
* - `platform-census-2026-08-31` — the 84 platform objects of the first
1919
* #13564 census (59 carrying the column, 25 not), measured at
20-
* `00d8f6541b`. Three of them have since left the tree
21-
* (`sys_view_definition`, `sys_saved_report`, `sys_report_schedule`); their
20+
* `00d8f6541b`. Four of them have since left the tree
21+
* (`sys_view_definition`, `sys_saved_report`, `sys_report_schedule`,
22+
* `sys_notification_subscription`); their
2223
* physical tables can still exist on an upgraded database, so they keep a
2324
* row.
2425
* - `platform-census-main` — objects the gated census on `main` lists and
@@ -473,6 +474,21 @@ export const ORGANIZATION_OWNERSHIP_INVENTORY: readonly InventoryEntry[] = [
473474
'the column, and a connection scoped to no organization was a legitimate row',
474475
},
475476

477+
// ── Platform: counted by the first census, retired since, table left in place (fate 4) ──
478+
{
479+
object: 'sys_notification_subscription',
480+
census: P1_ONLY,
481+
fate: 'report',
482+
citation:
483+
'ADR-0087 semantic entry sys-notification-subscription-retired (packages/spec/src/migrations/entries/semantic/' +
484+
'18.sys-notification-subscription-retired.ts): the ADR-0030 Layer-3 topic subscription no reader ever expanded ' +
485+
'into recipients, retired by maintainer ruling with its table left in place untouched. A physical table left ' +
486+
'by an older release is listed, its NULL rows reported, never attributed and never deleted',
487+
notNullExempt:
488+
'the object is retired and its table is left in place untouched, by ruling: no declaration anchors tenancy on ' +
489+
'the column any more, and nothing reads its rows',
490+
},
491+
476492
// ── Platform: attribution through a parent anchor (fate 3) ──
477493
{
478494
object: 'sys_file',
@@ -503,7 +519,7 @@ export const ORGANIZATION_OWNERSHIP_INVENTORY: readonly InventoryEntry[] = [
503519
{ object: 'sys_notification', census: P1, fate: 'attribution', citation: `${D7} (the notification family is recipient-anchored); the source record carries the organization`, anchors: [subject('source_object', 'source_id')] },
504520
{ object: 'sys_notification_delivery', census: P1, fate: 'attribution', citation: `${C7_FATE_3}; the delivery's notification carries the organization`, anchors: [parent('notification_id', 'sys_notification')] },
505521
{ object: 'sys_notification_receipt', census: P1, fate: 'attribution', citation: `${D7} (recipient-anchored); the receipt's notification carries the organization`, anchors: [parent('notification_id', 'sys_notification')] },
506-
...['sys_notification_preference', 'sys_notification_subscription', 'sys_inbox_message'].map(
522+
...['sys_notification_preference', 'sys_inbox_message'].map(
507523
(object): InventoryEntry => ({ object, census: P1, fate: 'attribution', citation: `${D7} (recipient-anchored); no anchor names the organization; ${D10_SINGLE}`, anchors: [] }),
508524
),
509525
{

‎packages/cli/src/utils/organization-ownership-plan.integration.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,48 @@ describe('ADR-0131 D10 preflight — the per-table plan', () => {
283283
expect(r).toMatchObject({ reason: 'uninventoried-platform-table', table: 'sys_scim_provider' });
284284
});
285285

286+
it('a database still holding the retired sys_notification_subscription plans, listing it as fate 4', async () => {
287+
// The table every database that booted before the retirement still holds,
288+
// in its last declared shape (the 18.0.0 upgrade-harness fixture's DDL).
289+
db.exec(`
290+
CREATE TABLE sys_notification_subscription (
291+
id TEXT PRIMARY KEY, created_at TEXT, updated_at TEXT, organization_id TEXT,
292+
created_by TEXT, updated_by TEXT, topic TEXT, principal TEXT, enabled INTEGER
293+
);
294+
INSERT INTO sys_notification_subscription VALUES
295+
('sub_1', '2026-07-01', '2026-07-01', 'org_b', 'usr_1', 'usr_1', 'billing.invoice', 'role:admin', 1),
296+
('sub_2', '2026-07-02', '2026-07-02', NULL, NULL, NULL, 'crm.lead', 'user:usr_1', 1);
297+
`);
298+
299+
for (const posture of ['isolated', 'single']) {
300+
const sub = table(await plan(posture), 'sys_notification_subscription');
301+
expect(sub, posture).toMatchObject({
302+
source: 'inventory',
303+
fate: 'report',
304+
physical: 'present',
305+
organizationColumn: 'present',
306+
rows: { total: 2, organizationNull: 1, stamped: 1 },
307+
fateCounts: { reportedRows: 1 },
308+
// Reported, never attributed — not even to the Default Organization under single.
309+
unattributable: [{ id: 'sub_2', reason: 'fate 4: reported, never guessed' }],
310+
notNull: { willReceive: false, reason: expect.stringContaining('left in place untouched') },
311+
});
312+
expect(sub.census, posture).toEqual(['platform-census-2026-08-31']);
313+
expect(sub.citation, posture).toContain('sys-notification-subscription-retired');
314+
}
315+
316+
// Control: without its row the same database is refused, by name.
317+
const r = await refusal(
318+
buildOrganizationOwnershipPlan({
319+
reader: reader(),
320+
posture: 'isolated',
321+
database: file,
322+
inventory: ORGANIZATION_OWNERSHIP_INVENTORY.filter((e) => e.object !== 'sys_notification_subscription'),
323+
}),
324+
);
325+
expect(r).toMatchObject({ reason: 'uninventoried-platform-table', table: 'sys_notification_subscription' });
326+
});
327+
286328
it('under single with no Default Organization (two organizations, none of them default), nothing is assigned and the reason says why', async () => {
287329
db.exec("UPDATE sys_organization SET slug = 'renamed' WHERE id = 'org_default'");
288330
const p = await plan('single');

‎packages/platform-objects/src/apps/translations/en.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ export const en: TranslationData = {
130130
nav_settings_feature_flags: { label: 'Feature Flags' },
131131
// Notifications (contributed by @objectstack/service-messaging)
132132
nav_notification_preferences: { label: 'Notification Preferences' },
133-
nav_notification_subscriptions: { label: 'Notification Subscriptions' },
134133
nav_notification_templates: { label: 'Notification Templates' },
135134

136135
// Diagnostics

‎packages/platform-objects/src/apps/translations/es-ES.source-hashes.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ export const esESSourceHashes: Readonly<Record<string, string>> = {
114114
'apps.setup.navigation.nav_settings_knowledge.label': '140348539a50d9d5',
115115
'apps.setup.navigation.nav_settings_feature_flags.label': '4e7e9e879ea1bcc3',
116116
'apps.setup.navigation.nav_notification_preferences.label': '8869f324c1bd545a',
117-
'apps.setup.navigation.nav_notification_subscriptions.label': '907cb60dda7b39c8',
118117
'apps.setup.navigation.nav_notification_templates.label': '9e270c87ef2578df',
119118
'apps.setup.navigation.nav_sessions.label': '673bde6e0c8a3ef9',
120119
'apps.setup.navigation.nav_audit_logs.label': 'b2fad40733012b18',

‎packages/platform-objects/src/apps/translations/es-ES.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ export const esES: TranslationData = {
9090
nav_settings_knowledge: { label: 'Conocimiento' },
9191
nav_settings_feature_flags: { label: 'Indicadores de Funcionalidad' },
9292
nav_notification_preferences: { label: 'Preferencias de Notificación' },
93-
nav_notification_subscriptions: { label: 'Suscripciones de Notificación' },
9493
nav_notification_templates: { label: 'Plantillas de Notificación' },
9594

9695
nav_sessions: { label: 'Sesiones' },

‎packages/platform-objects/src/apps/translations/ja-JP.source-hashes.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ export const jaJPSourceHashes: Readonly<Record<string, string>> = {
114114
'apps.setup.navigation.nav_settings_knowledge.label': '140348539a50d9d5',
115115
'apps.setup.navigation.nav_settings_feature_flags.label': '4e7e9e879ea1bcc3',
116116
'apps.setup.navigation.nav_notification_preferences.label': '8869f324c1bd545a',
117-
'apps.setup.navigation.nav_notification_subscriptions.label': '907cb60dda7b39c8',
118117
'apps.setup.navigation.nav_notification_templates.label': '9e270c87ef2578df',
119118
'apps.setup.navigation.nav_sessions.label': '673bde6e0c8a3ef9',
120119
'apps.setup.navigation.nav_audit_logs.label': 'b2fad40733012b18',

0 commit comments

Comments
 (0)